Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 74 additions & 58 deletions Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -402,69 +402,85 @@ Example::
task2 = tg.create_task(another_coro(...))
print(f"Both tasks have completed now: {task1.result()}, {task2.result()}")

The ``async with`` statement will wait for all tasks in the group to finish.
While waiting, new tasks may still be added to the group
(for example, by passing ``tg`` into one of the coroutines
and calling ``tg.create_task()`` in that coroutine). There is also opportunity to
request termination of the entire task group with ``tg.cancel()``, based on some condition.
Once the last task has finished and the ``async with`` block is exited,
no new tasks may be added to the group.

The first time any of the tasks belonging to the group fails
with an exception other than :exc:`asyncio.CancelledError`,
the remaining tasks in the group are cancelled.
No further tasks can then be added to the group.
At this point, if the body of the ``async with`` statement is still active
(i.e., :meth:`~object.__aexit__` hasn't been called yet),
the task directly containing the ``async with`` statement is also cancelled.
The resulting :exc:`asyncio.CancelledError` will interrupt an ``await``,
but it will not bubble out of the containing ``async with`` statement.

Once all tasks have finished, if any tasks have failed
with an exception other than :exc:`asyncio.CancelledError`,
those exceptions are combined in an
:exc:`ExceptionGroup` or :exc:`BaseExceptionGroup`
(as appropriate; see their documentation)
which is then raised.

Two base exceptions are treated specially:
If any task fails with :exc:`KeyboardInterrupt` or :exc:`SystemExit`,
the task group still cancels the remaining tasks and waits for them,
but then the initial :exc:`KeyboardInterrupt` or :exc:`SystemExit`
is re-raised instead of :exc:`ExceptionGroup` or :exc:`BaseExceptionGroup`.

If the body of the ``async with`` statement exits with an exception
(so :meth:`~object.__aexit__` is called with an exception set),
this is treated the same as if one of the tasks failed:
the remaining tasks are cancelled and then waited for,
and non-cancellation exceptions are grouped into an
exception group and raised.
The exception passed into :meth:`~object.__aexit__`,
unless it is :exc:`asyncio.CancelledError`,
is also included in the exception group.
The same special case is made for
:exc:`KeyboardInterrupt` and :exc:`SystemExit` as in the previous paragraph.
There is an additional special case made only for the body of the
``async with``: if it raises :exc:`GeneratorExit` and none of the
other tasks raise exceptions that would be reported, then the
:exc:`GeneratorExit` is reraised.

Task groups are careful not to mix up the internal cancellation used to
"wake up" their :meth:`~object.__aexit__` with cancellation requests
for the task in which they are running made by other parties.
A few points to keep in mind when using task groups:

* The ``async with`` statement will wait for all tasks in the group
to finish. While waiting, new tasks may still be added to the group
(for example, by passing ``tg`` into one of the coroutines and
calling ``tg.create_task()`` in that coroutine); once the last task
has finished and the ``async with`` block is exited, no new tasks
may be added.

* Termination of the entire task group may be requested with
``tg.cancel()``, based on some condition.

* If the group is shut down (e.g. because another task failed) before
a newly created task has started running, the task is cancelled
without its coroutine executing at all, not even to its first
``await``. To guarantee that the coroutine starts, create the task
eagerly with ``eager_start=True`` or use
:func:`asyncio.eager_task_factory`. For example::

async def job():
print("job started") # never printed
try:
await asyncio.sleep(1)
finally:
print("job cleaned up") # never printed

async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(job())
raise RuntimeError # shuts down the group before job() runs

With ``tg.create_task(job(), eager_start=True)``, ``job()`` runs up
to the ``await``, is cancelled there, and both messages are printed.

When any of the tasks belonging to the group fails with an exception
other than :exc:`asyncio.CancelledError` (or the body of the
``async with`` statement exits with an exception, which is treated
the same way):

* The first time this happens, the remaining tasks in the group are
cancelled and then waited for, and no further tasks can be added to
the group. If the body of the ``async with`` statement is still
active (i.e., :meth:`~object.__aexit__` hasn't been called yet),
the task directly containing the ``async with`` statement is also
cancelled. The resulting :exc:`asyncio.CancelledError` will
interrupt an ``await``, but it will not bubble out of the containing
``async with`` statement.

* Once all tasks have finished, the non-cancellation exceptions --
including the exception the body exited with, unless it is
:exc:`asyncio.CancelledError` -- are combined in an
:exc:`ExceptionGroup` or :exc:`BaseExceptionGroup`
(as appropriate; see their documentation), which is then raised.

* Some exceptions are treated specially: if any task fails with
:exc:`KeyboardInterrupt` or :exc:`SystemExit`, the task group still
cancels the remaining tasks and waits for them, but then the initial
:exc:`KeyboardInterrupt` or :exc:`SystemExit` is re-raised instead
of :exc:`ExceptionGroup` or :exc:`BaseExceptionGroup`.
Additionally, if the body of the ``async with`` statement raises
:exc:`GeneratorExit` and none of the other tasks raise exceptions
that would be reported, the :exc:`GeneratorExit` is re-raised.

Task groups are careful not to mix up the internal cancellation used
to "wake up" their :meth:`~object.__aexit__` with cancellation
requests for the task in which they are running made by other parties.
In particular, when one task group is syntactically nested in another,
and both experience an exception in one of their child tasks simultaneously,
the inner task group will process its exceptions, and then the outer task group
will receive another cancellation and process its own exceptions.
and both experience an exception in one of their child tasks
simultaneously, the inner task group will process its exceptions, and
then the outer task group will receive another cancellation and
process its own exceptions.

In the case where a task group is cancelled externally and also must
raise an :exc:`ExceptionGroup`, it will call the parent task's
:meth:`~asyncio.Task.cancel` method. This ensures that a
:meth:`~asyncio.Task.cancel` method. This ensures that a
:exc:`asyncio.CancelledError` will be raised at the next
:keyword:`await`, so the cancellation is not lost.

Task groups preserve the cancellation count
reported by :meth:`asyncio.Task.cancelling`.
:keyword:`await`, so the cancellation is not lost. Task groups also
preserve the cancellation count reported by
:meth:`asyncio.Task.cancelling`.

.. versionchanged:: 3.13

Expand Down
Loading