-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
gh-108951: add TaskGroup.cancel() #127214
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
base: main
Are you sure you want to change the base?
Changes from 10 commits
8ec2f60
907f1d0
2cfa1e6
bce1fb1
7754aad
452042d
f077241
8345647
7235c20
243db79
e36a1af
5ae5ee9
c83d853
5bba130
1ce0b40
e8617b4
8710b95
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 |
|---|---|---|
|
|
@@ -342,6 +342,33 @@ and reliable way to wait for all tasks in the group to finish. | |
|
|
||
| Close the given coroutine if the task group is not active. | ||
|
|
||
| .. method:: cancel() | ||
|
|
||
| Cancel the task group. | ||
|
|
||
| :meth:`~asyncio.Task.cancel` will be called on any tasks in the group that | ||
| aren't yet done, as well as the parent (body) of the group. This will | ||
| cause the task group context manager to exit *without* | ||
| :exc:`asyncio.CancelledError` being raised. | ||
|
|
||
| If :meth:`cancel` is called before entering the task group, the group will be | ||
| cancelled upon entry. This is useful for patterns where one piece of | ||
| code passes an unused :class:`asyncio.TaskGroup` instance to another in order to have | ||
| the ability to cancel anything run within the group. | ||
|
|
||
| :meth:`cancel` is idempotent and may be called after the task group has | ||
| already exited. | ||
|
|
||
| Ways to use :meth:`cancel`: | ||
|
|
||
| * call it from the task group body based on some condition or event | ||
|
Contributor
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. Probably you want code examples for all of these? |
||
| * pass the task group instance to child tasks via :meth:`create_task`, allowing a child | ||
| task to conditionally cancel the entire entire group | ||
| * pass the task group instance or bound :meth:`cancel` method to some other task *before* | ||
| opening the task group, allowing remote cancellation | ||
|
gvanrossum marked this conversation as resolved.
|
||
|
|
||
| .. versionadded:: next | ||
|
|
||
| Example:: | ||
|
|
||
| async def main(): | ||
|
|
@@ -414,53 +441,6 @@ reported by :meth:`asyncio.Task.cancelling`. | |
| Improved handling of simultaneous internal and external cancellations | ||
| and correct preservation of cancellation counts. | ||
|
|
||
| Terminating a Task Group | ||
|
gvanrossum marked this conversation as resolved.
Outdated
|
||
| ------------------------ | ||
|
|
||
| While terminating a task group is not natively supported by the standard | ||
| library, termination can be achieved by adding an exception-raising task | ||
| to the task group and ignoring the raised exception: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import asyncio | ||
| from asyncio import TaskGroup | ||
|
|
||
| class TerminateTaskGroup(Exception): | ||
| """Exception raised to terminate a task group.""" | ||
|
|
||
| async def force_terminate_task_group(): | ||
| """Used to force termination of a task group.""" | ||
| raise TerminateTaskGroup() | ||
|
|
||
| async def job(task_id, sleep_time): | ||
| print(f'Task {task_id}: start') | ||
| await asyncio.sleep(sleep_time) | ||
| print(f'Task {task_id}: done') | ||
|
|
||
| async def main(): | ||
| try: | ||
| async with TaskGroup() as group: | ||
| # spawn some tasks | ||
| group.create_task(job(1, 0.5)) | ||
| group.create_task(job(2, 1.5)) | ||
| # sleep for 1 second | ||
| await asyncio.sleep(1) | ||
| # add an exception-raising task to force the group to terminate | ||
| group.create_task(force_terminate_task_group()) | ||
| except* TerminateTaskGroup: | ||
| pass | ||
|
|
||
| asyncio.run(main()) | ||
|
|
||
| Expected output: | ||
|
|
||
| .. code-block:: text | ||
|
|
||
| Task 1: start | ||
| Task 2: start | ||
| Task 1: done | ||
|
|
||
| Sleeping | ||
| ======== | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ def __init__(self): | |
| self._errors = [] | ||
| self._base_error = None | ||
| self._on_completed_fut = None | ||
| self._cancel_on_enter = False | ||
|
|
||
| def __repr__(self): | ||
| info = [''] | ||
|
|
@@ -62,6 +63,8 @@ async def __aenter__(self): | |
| raise RuntimeError( | ||
| f'TaskGroup {self!r} cannot determine the parent task') | ||
| self._entered = True | ||
| if self._cancel_on_enter: | ||
| self.cancel() | ||
|
|
||
| return self | ||
|
|
||
|
|
@@ -177,6 +180,10 @@ async def _aexit(self, et, exc): | |
| finally: | ||
| exc = None | ||
|
|
||
| # If we wanted to raise an error, it would have been done explicitly | ||
| # above. Otherwise, either there is no error or we want to suppress | ||
| # the original error. | ||
|
belm0 marked this conversation as resolved.
Outdated
|
||
| return True | ||
|
gvanrossum marked this conversation as resolved.
|
||
|
|
||
| def create_task(self, coro, *, name=None, context=None): | ||
| """Create a new task in this group and return it. | ||
|
|
@@ -273,3 +280,30 @@ def _on_task_done(self, task): | |
| self._abort() | ||
| self._parent_cancel_requested = True | ||
| self._parent_task.cancel() | ||
|
|
||
| def cancel(self): | ||
|
Contributor
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. what do you think about supporting cancel messages here?
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 asked on Gitter, but I'm still unclear about how such a message would be accessed and surfaced.
Contributor
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. It could be logged by the task that gets cancelled, or useful in debugging
Contributor
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 would keep it as-is and maybe add a message in the follow-up PR; this PR is big enough for the review.
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.
My $0.02:
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. IOW, in case I wasn't clear, yes, we should add cancel message support. (Even if in the end we renamed the method to
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 think having a cancel message is going to encourage some anti-patterns such as parsing the string to determine code flow (this is why we stopped raising bare strings). If I were reviewing code using a message this way, I'd ask to refactor the code to signal "I cancelled you" in some more direct way. My sense is that this is much lower priority, and can always be reconsidered in the future.
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. Nevertheless it's a feature that most cancellable objects follow. And I don't think that anti-pattern has surfaced in practice. Why would TaskGroup be different in this respect? It is a feature third-party code uses, as a private protocol. Note that we don't even enforce that it is a string -- it can be anything, since it's just getting passed around. For an implementation example, see Future.cancel. |
||
| """Cancel the task group | ||
|
|
||
| `cancel()` will be called on any tasks in the group that aren't yet | ||
| done, as well as the parent (body) of the group. This will cause the | ||
| task group context manager to exit *without* `asyncio.CancelledError` | ||
| being raised. | ||
|
|
||
| If `cancel()` is called before entering the task group, the group will be | ||
| cancelled upon entry. This is useful for patterns where one piece of | ||
| code passes an unused TaskGroup instance to another in order to have | ||
| the ability to cancel anything run within the group. | ||
|
|
||
| `cancel()` is idempotent and may be called after the task group has | ||
| already exited. | ||
| """ | ||
| if not self._entered: | ||
| self._cancel_on_enter = True | ||
| return | ||
| if self._exiting and not self._tasks: | ||
| return | ||
| if not self._aborting: | ||
| self._abort() | ||
| if self._parent_task and not self._parent_cancel_requested: | ||
| self._parent_cancel_requested = True | ||
| self._parent_task.cancel() | ||
|
belm0 marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add :meth:`~asyncio.TaskGroup.cancel`. | ||
|
belm0 marked this conversation as resolved.
Outdated
|
||
Uh oh!
There was an error while loading. Please reload this page.