Skip to content

Commit 1850348

Browse files
committed
TUTORIAL.md: Document task group.
1 parent e9be6bf commit 1850348

File tree

1 file changed

+80
-3
lines changed

1 file changed

+80
-3
lines changed

v3/docs/TUTORIAL.md

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ REPL.
2727
3. [Synchronisation](./TUTORIAL.md#3-synchronisation)
2828
3.1 [Lock](./TUTORIAL.md#31-lock)
2929
3.2 [Event](./TUTORIAL.md#32-event)
30-
3.3 [gather](./TUTORIAL.md#33-gather)
30+
3.3 [Coordinating multiple tasks](./TUTORIAL.md#33-coordinating-multiple-tasks)
31+
     3.3.1 [gather](./TUTORIAL.md#331-gather)
32+
     3.3.2 [TaskGroups](./TUTORIAL.md#332-taskgroups)
3133
3.4 [Semaphore](./TUTORIAL.md#34-semaphore)
3234
     3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore)
3335
3.5 [Queue](./TUTORIAL.md#35-queue)
@@ -701,7 +703,15 @@ constant creation of tasks. Arguably the `Barrier` class is the best approach.
701703

702704
###### [Contents](./TUTORIAL.md#contents)
703705

704-
## 3.3 gather
706+
## 3.3 Coordinating multiple tasks
707+
708+
Several tasks may be launched together with the launching task pausing until
709+
all have completed. The `gather` mechanism is supported by CPython and
710+
MicroPython. CPython 3.11 adds a `TaskGroup` class which is particularly
711+
suited to applications where runtime exceptions may be encountered. It is not
712+
yet officially supported by MicroPython.
713+
714+
### 3.3.1 gather
705715

706716
This official `uasyncio` asynchronous method causes a number of tasks to run,
707717
pausing until all have either run to completion or been terminated by
@@ -714,7 +724,7 @@ res = await asyncio.gather(*tasks, return_exceptions=True)
714724
The keyword-only boolean arg `return_exceptions` determines the behaviour in
715725
the event of a cancellation or timeout of tasks. If `False` the `gather`
716726
terminates immediately, raising the relevant exception which should be trapped
717-
by the caller. If `True` the `gather` continues to block until all have either
727+
by the caller. If `True` the `gather` continues to pause until all have either
718728
run to completion or been terminated by cancellation or timeout. In this case
719729
tasks which have been terminated will return the exception object in the list
720730
of return values.
@@ -767,6 +777,73 @@ async def main():
767777
print('Cancelled')
768778
print('Result: ', res)
769779

780+
asyncio.run(main())
781+
```
782+
### 3.3.2 TaskGroups
783+
784+
The `TaskGroup` class is unofficially provided by
785+
[this PR](https://github.com/micropython/micropython/pull/8791). It is well
786+
suited to applications where one or more of a group of tasks is subject to
787+
runtime exceptions. A `TaskGroup` is instantiated in an asynchronous context
788+
manager. The `TaskGroup` instantiates member tasks. When all have run to
789+
completion the context manager terminates. Return values from member tasks
790+
cannot be retrieved. Results should be passed in other ways such as via bound
791+
variables, queues etc.
792+
793+
An exception in a member task not trapped by that task is propagated to the
794+
task that created the `TaskGroup`. All tasks in the `TaskGroup` then terminate
795+
in an orderly fashion: cleanup code in any `finally` clause will run. When all
796+
cleanup code has completed, the context manager completes, and execution passes
797+
to an exception handler in an outer scope.
798+
799+
If a member task is cancelled in code, that task terminates in an orderly way
800+
but the other members continue to run.
801+
802+
The following illustrates the basic salient points of using a `TaskGroup`:
803+
```python
804+
import uasyncio as asyncio
805+
async def foo(n):
806+
for x in range(10 + n):
807+
print(f"Task {n} running.")
808+
await asyncio.sleep(1 + n/10)
809+
print(f"Task {n} done")
810+
811+
async def main():
812+
async with asyncio.TaskGroup() as tg: # Context manager pauses until members terminate
813+
for n in range(4):
814+
tg.create_task(foo(n)) # tg.create_task() creates a member task
815+
print("TaskGroup done") # All tasks have terminated
816+
817+
asyncio.run(main())
818+
```
819+
This more complete example illustrates an exception which is not trapped by the
820+
member task. Cleanup code on all members runs when the exception occurs,
821+
followed by exception handling code in `main()`.
822+
```python
823+
import uasyncio as asyncio
824+
fail = True # Set False to demo normal completion
825+
async def foo(n):
826+
print(f"Task {n} running...")
827+
try:
828+
for x in range(10 + n):
829+
await asyncio.sleep(1 + n/10)
830+
if n==0 and x==5 and fail:
831+
raise OSError("Uncaught exception in task.")
832+
print(f"Task {n} done")
833+
finally:
834+
print(f"Task {n} cleanup")
835+
836+
async def main():
837+
try:
838+
async with asyncio.TaskGroup() as tg:
839+
for n in range(4):
840+
tg.create_task(foo(n))
841+
print("TaskGroup done") # Does not get here if a task throws exception
842+
except Exception as e:
843+
print(f'TaskGroup caught exception: "{e}"')
844+
finally:
845+
print("TaskGroup finally")
846+
770847
asyncio.run(main())
771848
```
772849

0 commit comments

Comments
 (0)