diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index c6f0fb8a50917a..689a6a6d2e0bd9 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -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