-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
gh-90908: Document asyncio.Task.cancelling() and asyncio.Task.uncancel() #95253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
708cb27
0203e01
4a6a2fe
ad49eb0
4c56381
13515f3
f0a215d
f3bcc6f
26cf287
9296af0
4114a79
50850de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -294,11 +294,14 @@ perform clean-up logic. In case :exc:`asyncio.CancelledError` | |
is explicitly caught, it should generally be propagated when | ||
clean-up is complete. Most code can safely ignore :exc:`asyncio.CancelledError`. | ||
|
||
Important asyncio components, like :class:`asyncio.TaskGroup` and the | ||
:func:`asyncio.timeout` context manager, are implemented using cancellation | ||
internally and might misbehave if a coroutine swallows | ||
:exc:`asyncio.CancelledError`. | ||
asyncio components that enable structured concurrency, like | ||
:class:`asyncio.TaskGroup` and the :func:`asyncio.timeout` context manager, | ||
gvanrossum marked this conversation as resolved.
Show resolved
Hide resolved
|
||
are implemented using cancellation internally and might misbehave if | ||
a coroutine swallows :exc:`asyncio.CancelledError`. In particular, | ||
they might :func:`uncancel <asyncio.Task.uncancel>` a task to properly | ||
isolate cancelling only a given structured block within the task's body. | ||
ambv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
.. _taskgroups: | ||
|
||
Task Groups | ||
=========== | ||
|
@@ -994,76 +997,6 @@ Task Object | |
Deprecation warning is emitted if *loop* is not specified | ||
and there is no running event loop. | ||
|
||
.. method:: cancel(msg=None) | ||
|
||
Request the Task to be cancelled. | ||
|
||
This arranges for a :exc:`CancelledError` exception to be thrown | ||
into the wrapped coroutine on the next cycle of the event loop. | ||
|
||
The coroutine then has a chance to clean up or even deny the | ||
request by suppressing the exception with a :keyword:`try` ... | ||
... ``except CancelledError`` ... :keyword:`finally` block. | ||
Therefore, unlike :meth:`Future.cancel`, :meth:`Task.cancel` does | ||
not guarantee that the Task will be cancelled, although | ||
suppressing cancellation completely is not common and is actively | ||
discouraged. | ||
|
||
.. versionchanged:: 3.9 | ||
Added the *msg* parameter. | ||
|
||
.. deprecated-removed:: 3.11 3.14 | ||
*msg* parameter is ambiguous when multiple :meth:`cancel` | ||
are called with different cancellation messages. | ||
The argument will be removed. | ||
|
||
.. _asyncio_example_task_cancel: | ||
|
||
The following example illustrates how coroutines can intercept | ||
the cancellation request:: | ||
|
||
async def cancel_me(): | ||
print('cancel_me(): before sleep') | ||
|
||
try: | ||
# Wait for 1 hour | ||
await asyncio.sleep(3600) | ||
except asyncio.CancelledError: | ||
print('cancel_me(): cancel sleep') | ||
raise | ||
finally: | ||
print('cancel_me(): after sleep') | ||
|
||
async def main(): | ||
# Create a "cancel_me" Task | ||
task = asyncio.create_task(cancel_me()) | ||
|
||
# Wait for 1 second | ||
await asyncio.sleep(1) | ||
|
||
task.cancel() | ||
try: | ||
await task | ||
except asyncio.CancelledError: | ||
print("main(): cancel_me is cancelled now") | ||
|
||
asyncio.run(main()) | ||
|
||
# Expected output: | ||
# | ||
# cancel_me(): before sleep | ||
# cancel_me(): cancel sleep | ||
# cancel_me(): after sleep | ||
# main(): cancel_me is cancelled now | ||
|
||
.. method:: cancelled() | ||
|
||
Return ``True`` if the Task is *cancelled*. | ||
|
||
The Task is *cancelled* when the cancellation was requested with | ||
:meth:`cancel` and the wrapped coroutine propagated the | ||
:exc:`CancelledError` exception thrown into it. | ||
|
||
.. method:: done() | ||
|
||
Return ``True`` if the Task is *done*. | ||
|
@@ -1177,3 +1110,153 @@ Task Object | |
in the :func:`repr` output of a task object. | ||
|
||
.. versionadded:: 3.8 | ||
|
||
.. method:: cancel(msg=None) | ||
|
||
Request the Task to be cancelled. | ||
|
||
This arranges for a :exc:`CancelledError` exception to be thrown | ||
into the wrapped coroutine on the next cycle of the event loop. | ||
|
||
The coroutine then has a chance to clean up or even deny the | ||
request by suppressing the exception with a :keyword:`try` ... | ||
... ``except CancelledError`` ... :keyword:`finally` block. | ||
Therefore, unlike :meth:`Future.cancel`, :meth:`Task.cancel` does | ||
not guarantee that the Task will be cancelled, although | ||
suppressing cancellation completely is not common and is actively | ||
discouraged. | ||
|
||
.. versionchanged:: 3.9 | ||
Added the *msg* parameter. | ||
|
||
.. deprecated-removed:: 3.11 3.14 | ||
*msg* parameter is ambiguous when multiple :meth:`cancel` | ||
are called with different cancellation messages. | ||
The argument will be removed. | ||
|
||
.. _asyncio_example_task_cancel: | ||
|
||
The following example illustrates how coroutines can intercept | ||
the cancellation request:: | ||
|
||
async def cancel_me(): | ||
print('cancel_me(): before sleep') | ||
|
||
try: | ||
# Wait for 1 hour | ||
await asyncio.sleep(3600) | ||
except asyncio.CancelledError: | ||
print('cancel_me(): cancel sleep') | ||
raise | ||
finally: | ||
print('cancel_me(): after sleep') | ||
|
||
async def main(): | ||
# Create a "cancel_me" Task | ||
task = asyncio.create_task(cancel_me()) | ||
|
||
# Wait for 1 second | ||
await asyncio.sleep(1) | ||
|
||
task.cancel() | ||
try: | ||
await task | ||
except asyncio.CancelledError: | ||
print("main(): cancel_me is cancelled now") | ||
|
||
asyncio.run(main()) | ||
|
||
# Expected output: | ||
# | ||
# cancel_me(): before sleep | ||
# cancel_me(): cancel sleep | ||
# cancel_me(): after sleep | ||
# main(): cancel_me is cancelled now | ||
|
||
.. method:: cancelled() | ||
|
||
Return ``True`` if the Task is *cancelled*. | ||
|
||
The Task is *cancelled* when the cancellation was requested with | ||
:meth:`cancel` and the wrapped coroutine propagated the | ||
:exc:`CancelledError` exception thrown into it. | ||
|
||
Comment on lines
+1122
to
+1191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part is unchanged, only moved down. IMO cancellation isn't as important as getting other things out of a task. Plus this move allows us to keep cancel-specific methods next to each other, |
||
.. method:: cancelling() | ||
|
||
Return the number of cancellation requests to this Task, i.e., | ||
the number of calls to :meth:`cancel`. | ||
|
||
Note that if this number is greater than zero but the Task is | ||
still executing, :meth:`cancelled` will still return ``False``. | ||
It's because this number can be lowered by calling :meth:`uncancel`, | ||
ambv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
which can lead to the task not being cancelled after all if the | ||
cancellation requests go down to zero. | ||
|
||
.. versionadded:: 3.11 | ||
|
||
.. method:: uncancel() | ||
|
||
Decrement the count of cancellation requests to this Task. | ||
|
||
Returns the remaining number of cancellation requests. | ||
|
||
Note that once execution of a cancelled task completed, further | ||
calls to :meth:`uncancel` are ineffective. | ||
|
||
.. versionadded:: 3.11 | ||
|
||
This method is used by asyncio's internals and isn't expected to be | ||
used by end-user code. In particular, if a Task gets successfully | ||
ambv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
uncancelled, this allows for elements of structured concurrency like | ||
:ref:`taskgroups` or and :func:`asyncio.timeout` to continue running, | ||
ambv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
isolating cancellation to the respective structured block. | ||
For example:: | ||
|
||
async def make_request_with_timeout(): | ||
try: | ||
async with asyncio.timeout(1): | ||
# Structured block affected by the timeout: | ||
await make_request() | ||
await make_another_request() | ||
except TimeoutError: | ||
log("There was a timeout") | ||
# Outer code not affected by the timeout: | ||
await unrelated_code() | ||
|
||
While the block with ``make_request()`` and ``make_another_request()`` | ||
might get cancelled due to the timeout, ``unrelated_code()`` should | ||
continue running even in case of the timeout. This can be | ||
implemented with :meth:`uncancel` as follows:: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if we need to give an implementation of a structured concurrency primitive as an example in the docs, given that we don't expect (or want!) people to do this. I also don't have time to review the example carefully enough to trust it doesn't have bugs that would be replicated if people copy this example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved the example to tests but kept it there as reference docs for our own purposes. |
||
|
||
async def make_request_with_timeout(): | ||
task = asyncio.current_task() | ||
loop = task.get_loop() | ||
i_called_cancel = False | ||
|
||
def on_timeout(): | ||
nonlocal i_called_cancel | ||
i_called_cancel = True | ||
task.cancel() | ||
|
||
timeout_handle = loop.call_later(1, on_timeout) | ||
try: | ||
try: | ||
# Structured block affected by the timeout | ||
await make_request() | ||
await make_another_request() | ||
finally: | ||
timeout_handle.cancel() | ||
if ( | ||
i_called_cancel | ||
and task.uncancel() == 0 | ||
and sys.exc_info()[0] is asyncio.CancelledError | ||
): | ||
raise TimeoutError | ||
except TimeoutError: | ||
log("There was a timeout") | ||
|
||
# Outer code not affected by the timeout: | ||
await unrelated_code() | ||
|
||
:class:`TaskGroup` context managers use :func:`uncancel` in | ||
a similar fashion. |
Uh oh!
There was an error while loading. Please reload this page.