This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Introduce task groups to asyncio and change task cancellation semantics
Type: Stage: patch review
Components: asyncio Versions: Python 3.11
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: gvanrossum Nosy List: asvetlov, dhalbert, gvanrossum, iritkatriel, njs, tinchester, yduprat, yselivanov
Priority: normal Keywords: needs review, patch

Created on 2022-02-14 20:44 by gvanrossum, last changed 2022-04-11 14:59 by admin.

Pull Requests
URL Status Linked Edit
PR 31270 merged gvanrossum, 2022-02-14 20:44
PR 31398 merged gvanrossum, 2022-02-17 23:58
PR 31409 merged asvetlov, 2022-02-18 16:09
PR 31411 merged gvanrossum, 2022-02-18 17:08
PR 31513 merged tinchester, 2022-02-23 21:31
PR 31559 merged tinchester, 2022-02-25 01:17
Messages (10)
msg413260 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-14 20:44
After some conversations with Yury, and encouraged by the SC's approval of PEP 654, I am proposing to add a new class, asyncio.TaskGroup, which introduces structured concurrency similar to nurseries in Trio.

I started with EdgeDb's TaskGroup implementation (https://github.com/edgedb/edgedb/blob/master/edb/common/taskgroup.py) and tweaked it only slightly. I also changed a few things in asyncio.Task (see below).

The key change I made to EdgeDb's TaskGroup is that subtasks can keep spawning more subtasks while __aexit__ is running; __aexit__ exits once the last subtask is done. I made this change after consulting some Trio folks, who knew of real-world use cases for this behavior, and did not know of real-world code in need of prohibiting task creation as soon as __aexit__ starts running. I added some tests for the new behavior; none of the existing tests needed to be adjusted to accommodate this change.

(For other changes relative to the EdgeDb's TaskGroup, see GH-31270.)

In order to avoid the need to monkey-patch the parent task, I added two new methods to asyncio.Task, .cancelled() and .uncancel(), that manage a flag corresponding to __cancel_requested__ in EdgeDb's TaskGroup. 

**This introduces a change in behavior around task cancellation:**

* A task that catches CancelledError is allowed to run undisturbed (ignoring further .cancel() calls and allowing any number of await calls!) until it either exits or calls .uncancel().

This change in semantics did not cause any asyncio unittests to fail. However, it may be surprising (especially to Trio folks, where the semantics are pretty much the opposite, once a Trio task is cancelled all further await calls in that task fail unless explicitly shielded).

For the TaskGroup tests to pass, we require a flag that is not cleared. However, it is probably not really required to ignore subsequent .cancel() calls until .uncancel() is called. This just seemed more consistent, and it is what @asvetlov proposed above and implemented in GH-31313 (using a property .__cancel_requested__ as the API).
msg413305 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-15 23:42
New changeset 602630ac1855e38ef06361c68f6e216375a06180 by Guido van Rossum in branch 'main':
bpo-46752: Add TaskGroup; add Task..cancelled(),.uncancel() (GH-31270)
https://github.com/python/cpython/commit/602630ac1855e38ef06361c68f6e216375a06180
msg413306 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-15 23:45
Remaining TODO list:

- Add a test showing the need for the .uncancel() call in __aexit__()
  (currently on line 97). Dropping that line does not cause any tests
  to fail.
- Ensure the taskgroup tests are run with the C and Python Task
  implementations.
- Rename tests to have meaningful names.
- I have a few ideas for minor cleanups that I will do later.
- Documentation and What's New entry (in a separate PR, probably).
- Update the docs in a few places to de-prioritize asyncio.gather()
  and steer people towards TaskGroups.

(We could also add something like Trio's cancel scopes, e.g. based on
Andrew Svetlov's async-timeout, which has a mature API.
But that should be a separate bpo issue.)
msg413335 - (view) Author: Dan Halbert (dhalbert) Date: 2022-02-16 15:29
For your TODO list (not sure how else to communicate this):

I agree with the de-emphasis of gather(). I think adding another version of gather() that cancels all the remaining tasks if one fails would also be good, unless you think it is completely redundant due to TaskGroups. This idea was originally mentioned in https://bugs.python.org/issue31452 as a bug, and determined to be "works as designed". So now making an all-cancel() version of gather() is an idiom that people keep recoding, e.g. https://stackoverflow.com/questions/59073556/how-to-cancel-all-remaining-tasks-in-gather-if-one-fails.
msg413344 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-16 17:53
@dhalbert, it's probably better to file a new issue if you want changes to gather(). Although I suppose that if we want to deemphasize it, we shouldn't be adding new features to it. My own new feature idea would be to have it wait for all tasks and then if there are any exceptions, raise an ExceptionGroup. That (like any new gather() behaviors) would require a new keyword-only flag to gather(). If we're going to deemphasize it I might not bother though.

There's one thing that gather() does that TaskGroup doesn't: it gives us the return values from the tasks. The question is whether that's useful. If it is maybe we should *not* deepmhasize gather() quite as much and then adding new features would be okay.
msg413346 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-16 17:59
I've created a separate issue for cancel scopes: bpo-46771.
msg413348 - (view) Author: Yury Selivanov (yselivanov) * (Python committer) Date: 2022-02-16 18:14
> There's one thing that gather() does that TaskGroup doesn't: it gives us the return values from the tasks.

That's easy to do with task groups too:

  async with TaskGroup() as g:
      r1 = g.create_task(coro1())
      r2 = g.create_task(coro2())

  print(r1.result())

  # or
  print(await r2)  # I *think* this should work
msg413353 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-16 18:37
I have a PR up to typeshed to add the new Task methods and a new stub file taskgroups.pyi: https://github.com/python/typeshed/pull/7240
msg413467 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2022-02-18 05:30
New changeset d85121660ea50bbe8fbd31797aa6e4afe0850388 by Guido van Rossum in branch 'main':
bpo-46752: Slight improvements to TaskGroup API (GH-31398)
https://github.com/python/cpython/commit/d85121660ea50bbe8fbd31797aa6e4afe0850388
msg413577 - (view) Author: Andrew Svetlov (asvetlov) * (Python committer) Date: 2022-02-20 10:07
New changeset e7130c2e8c6abfaf04b209bd5b239059eda024b9 by Andrew Svetlov in branch 'main':
bpo-46752: Uniform TaskGroup.__repr__ (GH-31409)
https://github.com/python/cpython/commit/e7130c2e8c6abfaf04b209bd5b239059eda024b9
History
Date User Action Args
2022-04-11 14:59:56adminsetgithub: 90908
2022-03-21 10:25:32iritkatriellinkissue32754 superseder
2022-02-25 15:33:21ydupratsetnosy: + yduprat
2022-02-25 01:17:45tinchestersetpull_requests: + pull_request29682
2022-02-23 21:31:32tinchestersetnosy: + tinchester
pull_requests: + pull_request29664
2022-02-20 10:07:15asvetlovsetmessages: + msg413577
2022-02-18 17:08:44gvanrossumsetpull_requests: + pull_request29551
2022-02-18 16:09:41asvetlovsetpull_requests: + pull_request29550
2022-02-18 05:30:58gvanrossumsetmessages: + msg413467
2022-02-17 23:58:33gvanrossumsetpull_requests: + pull_request29541
2022-02-16 18:37:34gvanrossumsetmessages: + msg413353
2022-02-16 18:14:17yselivanovsetmessages: + msg413348
2022-02-16 18:01:54gvanrossumsetnosy: + dhalbert
2022-02-16 17:59:51gvanrossumsetmessages: + msg413346
2022-02-16 17:53:02gvanrossumsetnosy: - dhalbert
messages: + msg413344
2022-02-16 15:29:01dhalbertsetnosy: + dhalbert
messages: + msg413335
2022-02-15 23:45:51gvanrossumsetmessages: + msg413306
2022-02-15 23:42:23gvanrossumsetmessages: + msg413305
2022-02-15 02:38:18xtreaksetnosy: + njs
2022-02-14 20:44:51gvanrossumsetkeywords: + patch
pull_requests: + pull_request29493
2022-02-14 20:44:29gvanrossumcreate