-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
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 1 commit
708cb27
0203e01
4a6a2fe
ad49eb0
4c56381
13515f3
f0a215d
f3bcc6f
26cf287
9296af0
4114a79
50850de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
uncancel() example, move cancellation methods down in docs
Co-authored-by: Thomas Grainger <tagrain@gmail.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -293,14 +293,15 @@ It is recommended that coroutines use ``try/finally`` blocks to robustly | |
| 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`. | ||
| If a task needs to continue despite receiving an :exc:`asyncio.CancelledError`, | ||
| it should :func:`uncancel itself <asyncio.Task.uncancel>`. | ||
|
|
||
| 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.
Outdated
|
||
| 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.
Outdated
|
||
|
|
||
| .. _taskgroups: | ||
|
|
||
| Task Groups | ||
| =========== | ||
|
|
@@ -996,106 +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:: 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`, | ||
| which can lead to the task not being cancelled after all if the | ||
| cancellation requests go down to zero. | ||
|
|
||
| .. method:: uncancel() | ||
|
|
||
| Decrement the count of cancellation requests to this Task. | ||
|
|
||
| Returns the remaining number of cancellation requests. | ||
|
|
||
| This should be used by tasks that catch :exc:`CancelledError` | ||
| and wish to continue indefinitely until they are cancelled again:: | ||
|
|
||
| async def resilient_task(): | ||
| try: | ||
| await do_work() | ||
| except asyncio.CancelledError: | ||
| asyncio.current_task().uncancel() | ||
| await do_work() | ||
|
|
||
| Note that once execution of a cancelled task completed, further | ||
| calls to :meth:`uncancel` are ineffective. | ||
|
|
||
| .. method:: done() | ||
|
|
||
| Return ``True`` if the Task is *done*. | ||
|
|
@@ -1209,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
Contributor
Author
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.
Outdated
|
||
| 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.
|
||
| 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.
Outdated
|
||
| 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:: | ||
|
Member
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.
Contributor
Author
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.