Skip to content

Commit 862befd

Browse files
gh-116048: restructure task group docs and add docs for eager execution (#156102)
1 parent e2cb8b9 commit 862befd

1 file changed

Lines changed: 74 additions & 58 deletions

File tree

Doc/library/asyncio-task.rst

Lines changed: 74 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -402,69 +402,85 @@ Example::
402402
task2 = tg.create_task(another_coro(...))
403403
print(f"Both tasks have completed now: {task1.result()}, {task2.result()}")
404404

405-
The ``async with`` statement will wait for all tasks in the group to finish.
406-
While waiting, new tasks may still be added to the group
407-
(for example, by passing ``tg`` into one of the coroutines
408-
and calling ``tg.create_task()`` in that coroutine). There is also opportunity to
409-
request termination of the entire task group with ``tg.cancel()``, based on some condition.
410-
Once the last task has finished and the ``async with`` block is exited,
411-
no new tasks may be added to the group.
412-
413-
The first time any of the tasks belonging to the group fails
414-
with an exception other than :exc:`asyncio.CancelledError`,
415-
the remaining tasks in the group are cancelled.
416-
No further tasks can then be added to the group.
417-
At this point, if the body of the ``async with`` statement is still active
418-
(i.e., :meth:`~object.__aexit__` hasn't been called yet),
419-
the task directly containing the ``async with`` statement is also cancelled.
420-
The resulting :exc:`asyncio.CancelledError` will interrupt an ``await``,
421-
but it will not bubble out of the containing ``async with`` statement.
422-
423-
Once all tasks have finished, if any tasks have failed
424-
with an exception other than :exc:`asyncio.CancelledError`,
425-
those exceptions are combined in an
426-
:exc:`ExceptionGroup` or :exc:`BaseExceptionGroup`
427-
(as appropriate; see their documentation)
428-
which is then raised.
429-
430-
Two base exceptions are treated specially:
431-
If any task fails with :exc:`KeyboardInterrupt` or :exc:`SystemExit`,
432-
the task group still cancels the remaining tasks and waits for them,
433-
but then the initial :exc:`KeyboardInterrupt` or :exc:`SystemExit`
434-
is re-raised instead of :exc:`ExceptionGroup` or :exc:`BaseExceptionGroup`.
435-
436-
If the body of the ``async with`` statement exits with an exception
437-
(so :meth:`~object.__aexit__` is called with an exception set),
438-
this is treated the same as if one of the tasks failed:
439-
the remaining tasks are cancelled and then waited for,
440-
and non-cancellation exceptions are grouped into an
441-
exception group and raised.
442-
The exception passed into :meth:`~object.__aexit__`,
443-
unless it is :exc:`asyncio.CancelledError`,
444-
is also included in the exception group.
445-
The same special case is made for
446-
:exc:`KeyboardInterrupt` and :exc:`SystemExit` as in the previous paragraph.
447-
There is an additional special case made only for the body of the
448-
``async with``: if it raises :exc:`GeneratorExit` and none of the
449-
other tasks raise exceptions that would be reported, then the
450-
:exc:`GeneratorExit` is reraised.
451-
452-
Task groups are careful not to mix up the internal cancellation used to
453-
"wake up" their :meth:`~object.__aexit__` with cancellation requests
454-
for the task in which they are running made by other parties.
405+
A few points to keep in mind when using task groups:
406+
407+
* The ``async with`` statement will wait for all tasks in the group
408+
to finish. While waiting, new tasks may still be added to the group
409+
(for example, by passing ``tg`` into one of the coroutines and
410+
calling ``tg.create_task()`` in that coroutine); once the last task
411+
has finished and the ``async with`` block is exited, no new tasks
412+
may be added.
413+
414+
* Termination of the entire task group may be requested with
415+
``tg.cancel()``, based on some condition.
416+
417+
* If the group is shut down (e.g. because another task failed) before
418+
a newly created task has started running, the task is cancelled
419+
without its coroutine executing at all, not even to its first
420+
``await``. To guarantee that the coroutine starts, create the task
421+
eagerly with ``eager_start=True`` or use
422+
:func:`asyncio.eager_task_factory`. For example::
423+
424+
async def job():
425+
print("job started") # never printed
426+
try:
427+
await asyncio.sleep(1)
428+
finally:
429+
print("job cleaned up") # never printed
430+
431+
async def main():
432+
async with asyncio.TaskGroup() as tg:
433+
tg.create_task(job())
434+
raise RuntimeError # shuts down the group before job() runs
435+
436+
With ``tg.create_task(job(), eager_start=True)``, ``job()`` runs up
437+
to the ``await``, is cancelled there, and both messages are printed.
438+
439+
When any of the tasks belonging to the group fails with an exception
440+
other than :exc:`asyncio.CancelledError` (or the body of the
441+
``async with`` statement exits with an exception, which is treated
442+
the same way):
443+
444+
* The first time this happens, the remaining tasks in the group are
445+
cancelled and then waited for, and no further tasks can be added to
446+
the group. If the body of the ``async with`` statement is still
447+
active (i.e., :meth:`~object.__aexit__` hasn't been called yet),
448+
the task directly containing the ``async with`` statement is also
449+
cancelled. The resulting :exc:`asyncio.CancelledError` will
450+
interrupt an ``await``, but it will not bubble out of the containing
451+
``async with`` statement.
452+
453+
* Once all tasks have finished, the non-cancellation exceptions --
454+
including the exception the body exited with, unless it is
455+
:exc:`asyncio.CancelledError` -- are combined in an
456+
:exc:`ExceptionGroup` or :exc:`BaseExceptionGroup`
457+
(as appropriate; see their documentation), which is then raised.
458+
459+
* Some exceptions are treated specially: if any task fails with
460+
:exc:`KeyboardInterrupt` or :exc:`SystemExit`, the task group still
461+
cancels the remaining tasks and waits for them, but then the initial
462+
:exc:`KeyboardInterrupt` or :exc:`SystemExit` is re-raised instead
463+
of :exc:`ExceptionGroup` or :exc:`BaseExceptionGroup`.
464+
Additionally, if the body of the ``async with`` statement raises
465+
:exc:`GeneratorExit` and none of the other tasks raise exceptions
466+
that would be reported, the :exc:`GeneratorExit` is re-raised.
467+
468+
Task groups are careful not to mix up the internal cancellation used
469+
to "wake up" their :meth:`~object.__aexit__` with cancellation
470+
requests for the task in which they are running made by other parties.
455471
In particular, when one task group is syntactically nested in another,
456-
and both experience an exception in one of their child tasks simultaneously,
457-
the inner task group will process its exceptions, and then the outer task group
458-
will receive another cancellation and process its own exceptions.
472+
and both experience an exception in one of their child tasks
473+
simultaneously, the inner task group will process its exceptions, and
474+
then the outer task group will receive another cancellation and
475+
process its own exceptions.
459476

460477
In the case where a task group is cancelled externally and also must
461478
raise an :exc:`ExceptionGroup`, it will call the parent task's
462-
:meth:`~asyncio.Task.cancel` method. This ensures that a
479+
:meth:`~asyncio.Task.cancel` method. This ensures that a
463480
:exc:`asyncio.CancelledError` will be raised at the next
464-
:keyword:`await`, so the cancellation is not lost.
465-
466-
Task groups preserve the cancellation count
467-
reported by :meth:`asyncio.Task.cancelling`.
481+
:keyword:`await`, so the cancellation is not lost. Task groups also
482+
preserve the cancellation count reported by
483+
:meth:`asyncio.Task.cancelling`.
468484

469485
.. versionchanged:: 3.13
470486

0 commit comments

Comments
 (0)