Skip to content

Commit 0203e01

Browse files
ambvgraingert
andcommitted
Reword uncancel() example, move cancellation methods down in docs
Co-authored-by: Thomas Grainger <[email protected]>
1 parent 708cb27 commit 0203e01

File tree

1 file changed

+157
-106
lines changed

1 file changed

+157
-106
lines changed

Doc/library/asyncio-task.rst

+157-106
Original file line numberDiff line numberDiff line change
@@ -293,14 +293,15 @@ It is recommended that coroutines use ``try/finally`` blocks to robustly
293293
perform clean-up logic. In case :exc:`asyncio.CancelledError`
294294
is explicitly caught, it should generally be propagated when
295295
clean-up is complete. Most code can safely ignore :exc:`asyncio.CancelledError`.
296-
If a task needs to continue despite receiving an :exc:`asyncio.CancelledError`,
297-
it should :func:`uncancel itself <asyncio.Task.uncancel>`.
298296

299-
Important asyncio components, like :class:`asyncio.TaskGroup` and the
300-
:func:`asyncio.timeout` context manager, are implemented using cancellation
301-
internally and might misbehave if a coroutine swallows
302-
:exc:`asyncio.CancelledError`.
297+
asyncio components that enable structured concurrency, like
298+
:class:`asyncio.TaskGroup` and the :func:`asyncio.timeout` context manager,
299+
are implemented using cancellation internally and might misbehave if
300+
a coroutine swallows :exc:`asyncio.CancelledError`. In particular,
301+
they might :func:`uncancel <asyncio.Task.uncancel>` a task to properly
302+
isolate cancelling only a given structured block within the task's body.
303303

304+
.. _taskgroups:
304305

305306
Task Groups
306307
===========
@@ -996,106 +997,6 @@ Task Object
996997
Deprecation warning is emitted if *loop* is not specified
997998
and there is no running event loop.
998999

999-
.. method:: cancel(msg=None)
1000-
1001-
Request the Task to be cancelled.
1002-
1003-
This arranges for a :exc:`CancelledError` exception to be thrown
1004-
into the wrapped coroutine on the next cycle of the event loop.
1005-
1006-
The coroutine then has a chance to clean up or even deny the
1007-
request by suppressing the exception with a :keyword:`try` ...
1008-
... ``except CancelledError`` ... :keyword:`finally` block.
1009-
Therefore, unlike :meth:`Future.cancel`, :meth:`Task.cancel` does
1010-
not guarantee that the Task will be cancelled, although
1011-
suppressing cancellation completely is not common and is actively
1012-
discouraged.
1013-
1014-
.. versionchanged:: 3.9
1015-
Added the *msg* parameter.
1016-
1017-
.. deprecated-removed:: 3.11 3.14
1018-
*msg* parameter is ambiguous when multiple :meth:`cancel`
1019-
are called with different cancellation messages.
1020-
The argument will be removed.
1021-
1022-
.. _asyncio_example_task_cancel:
1023-
1024-
The following example illustrates how coroutines can intercept
1025-
the cancellation request::
1026-
1027-
async def cancel_me():
1028-
print('cancel_me(): before sleep')
1029-
1030-
try:
1031-
# Wait for 1 hour
1032-
await asyncio.sleep(3600)
1033-
except asyncio.CancelledError:
1034-
print('cancel_me(): cancel sleep')
1035-
raise
1036-
finally:
1037-
print('cancel_me(): after sleep')
1038-
1039-
async def main():
1040-
# Create a "cancel_me" Task
1041-
task = asyncio.create_task(cancel_me())
1042-
1043-
# Wait for 1 second
1044-
await asyncio.sleep(1)
1045-
1046-
task.cancel()
1047-
try:
1048-
await task
1049-
except asyncio.CancelledError:
1050-
print("main(): cancel_me is cancelled now")
1051-
1052-
asyncio.run(main())
1053-
1054-
# Expected output:
1055-
#
1056-
# cancel_me(): before sleep
1057-
# cancel_me(): cancel sleep
1058-
# cancel_me(): after sleep
1059-
# main(): cancel_me is cancelled now
1060-
1061-
.. method:: cancelled()
1062-
1063-
Return ``True`` if the Task is *cancelled*.
1064-
1065-
The Task is *cancelled* when the cancellation was requested with
1066-
:meth:`cancel` and the wrapped coroutine propagated the
1067-
:exc:`CancelledError` exception thrown into it.
1068-
1069-
.. method:: cancelling()
1070-
1071-
Return the number of cancellation requests to this Task, i.e.,
1072-
the number of calls to :meth:`cancel`.
1073-
1074-
Note that if this number is greater than zero but the Task is
1075-
still executing, :meth:`cancelled` will still return ``False``.
1076-
It's because this number can be lowered by calling :meth:`uncancel`,
1077-
which can lead to the task not being cancelled after all if the
1078-
cancellation requests go down to zero.
1079-
1080-
.. method:: uncancel()
1081-
1082-
Decrement the count of cancellation requests to this Task.
1083-
1084-
Returns the remaining number of cancellation requests.
1085-
1086-
This should be used by tasks that catch :exc:`CancelledError`
1087-
and wish to continue indefinitely until they are cancelled again::
1088-
1089-
async def resilient_task():
1090-
try:
1091-
await do_work()
1092-
except asyncio.CancelledError:
1093-
asyncio.current_task().uncancel()
1094-
await do_work()
1095-
1096-
Note that once execution of a cancelled task completed, further
1097-
calls to :meth:`uncancel` are ineffective.
1098-
10991000
.. method:: done()
11001001

11011002
Return ``True`` if the Task is *done*.
@@ -1209,3 +1110,153 @@ Task Object
12091110
in the :func:`repr` output of a task object.
12101111

12111112
.. versionadded:: 3.8
1113+
1114+
.. method:: cancel(msg=None)
1115+
1116+
Request the Task to be cancelled.
1117+
1118+
This arranges for a :exc:`CancelledError` exception to be thrown
1119+
into the wrapped coroutine on the next cycle of the event loop.
1120+
1121+
The coroutine then has a chance to clean up or even deny the
1122+
request by suppressing the exception with a :keyword:`try` ...
1123+
... ``except CancelledError`` ... :keyword:`finally` block.
1124+
Therefore, unlike :meth:`Future.cancel`, :meth:`Task.cancel` does
1125+
not guarantee that the Task will be cancelled, although
1126+
suppressing cancellation completely is not common and is actively
1127+
discouraged.
1128+
1129+
.. versionchanged:: 3.9
1130+
Added the *msg* parameter.
1131+
1132+
.. deprecated-removed:: 3.11 3.14
1133+
*msg* parameter is ambiguous when multiple :meth:`cancel`
1134+
are called with different cancellation messages.
1135+
The argument will be removed.
1136+
1137+
.. _asyncio_example_task_cancel:
1138+
1139+
The following example illustrates how coroutines can intercept
1140+
the cancellation request::
1141+
1142+
async def cancel_me():
1143+
print('cancel_me(): before sleep')
1144+
1145+
try:
1146+
# Wait for 1 hour
1147+
await asyncio.sleep(3600)
1148+
except asyncio.CancelledError:
1149+
print('cancel_me(): cancel sleep')
1150+
raise
1151+
finally:
1152+
print('cancel_me(): after sleep')
1153+
1154+
async def main():
1155+
# Create a "cancel_me" Task
1156+
task = asyncio.create_task(cancel_me())
1157+
1158+
# Wait for 1 second
1159+
await asyncio.sleep(1)
1160+
1161+
task.cancel()
1162+
try:
1163+
await task
1164+
except asyncio.CancelledError:
1165+
print("main(): cancel_me is cancelled now")
1166+
1167+
asyncio.run(main())
1168+
1169+
# Expected output:
1170+
#
1171+
# cancel_me(): before sleep
1172+
# cancel_me(): cancel sleep
1173+
# cancel_me(): after sleep
1174+
# main(): cancel_me is cancelled now
1175+
1176+
.. method:: cancelled()
1177+
1178+
Return ``True`` if the Task is *cancelled*.
1179+
1180+
The Task is *cancelled* when the cancellation was requested with
1181+
:meth:`cancel` and the wrapped coroutine propagated the
1182+
:exc:`CancelledError` exception thrown into it.
1183+
1184+
.. method:: cancelling()
1185+
1186+
Return the number of cancellation requests to this Task, i.e.,
1187+
the number of calls to :meth:`cancel`.
1188+
1189+
Note that if this number is greater than zero but the Task is
1190+
still executing, :meth:`cancelled` will still return ``False``.
1191+
It's because this number can be lowered by calling :meth:`uncancel`,
1192+
which can lead to the task not being cancelled after all if the
1193+
cancellation requests go down to zero.
1194+
1195+
.. versionadded:: 3.11
1196+
1197+
.. method:: uncancel()
1198+
1199+
Decrement the count of cancellation requests to this Task.
1200+
1201+
Returns the remaining number of cancellation requests.
1202+
1203+
Note that once execution of a cancelled task completed, further
1204+
calls to :meth:`uncancel` are ineffective.
1205+
1206+
.. versionadded:: 3.11
1207+
1208+
This method is used by asyncio's internals and isn't expected to be
1209+
used by end-user code. In particular, if a Task gets successfully
1210+
uncancelled, this allows for elements of structured concurrency like
1211+
:ref:`taskgroups` or and :func:`asyncio.timeout` to continue running,
1212+
isolating cancellation to the respective structured block.
1213+
For example::
1214+
1215+
async def make_request_with_timeout():
1216+
try:
1217+
async with asyncio.timeout(1):
1218+
# Structured block affected by the timeout:
1219+
await make_request()
1220+
await make_another_request()
1221+
except TimeoutError:
1222+
log("There was a timeout")
1223+
# Outer code not affected by the timeout:
1224+
await unrelated_code()
1225+
1226+
While the block with ``make_request()`` and ``make_another_request()``
1227+
might get cancelled due to the timeout, ``unrelated_code()`` should
1228+
continue running even in case of the timeout. This can be
1229+
implemented with :meth:`uncancel` as follows::
1230+
1231+
async def make_request_with_timeout():
1232+
task = asyncio.current_task()
1233+
loop = task.get_loop()
1234+
i_called_cancel = False
1235+
1236+
def on_timeout():
1237+
nonlocal i_called_cancel
1238+
i_called_cancel = True
1239+
task.cancel()
1240+
1241+
timeout_handle = loop.call_later(1, on_timeout)
1242+
try:
1243+
try:
1244+
# Structured block affected by the timeout
1245+
await make_request()
1246+
await make_another_request()
1247+
finally:
1248+
timeout_handle.cancel()
1249+
if (
1250+
i_called_cancel
1251+
and task.uncancel() == 0
1252+
and sys.exc_info()[0] is asyncio.CancelledError
1253+
):
1254+
raise TimeoutError
1255+
except TimeoutError:
1256+
log("There was a timeout")
1257+
1258+
# Outer code not affected by the timeout:
1259+
await unrelated_code()
1260+
1261+
:class:`TaskGroup` context managers use :func:`uncancel` in
1262+
a similar fashion.

0 commit comments

Comments
 (0)