GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Add eager task creation API to asyncio · Issue #97696 · python/cpython

Add eager task creation API to asyncio #97696

New issue
Copy link
New issue
Copy link
Closed
#102853
Closed
Add eager task creation API to asyncio#97696
#102853
Copy link
Labels
3.12only security fixesonly security fixesperformancePerformance or resource usagePerformance or resource usagerelease-blockertopic-asynciotype-featureA feature request or enhancementA feature request or enhancement

Description

@jbower-fb
jbower-fb
opened on Sep 30, 2022
Issue body actions

Feature or enhancement

We propose adding “eager” coroutine execution support to asyncio.TaskGroup via a new method enqueue() [1].

TaskGroup.enqueue() would have the same signature as TaskGroup.create_task() but eagerly perform the first step of the passed coroutine’s execution immediately. If the coroutine completes without yielding, the result of enqueue() would be an object which behaves like a completed asyncio.Task. Otherwise, enqueue() behaves the same as TaskGroup.create_task(), returning a pending asyncio.Task.

The reason for a new method, rather than changing the implementation of TaskGroup.create_task() is this new method introduces a small semantic difference. For example in:

async def coro(): ...

async with TaskGroup() as tg:
  tg.enqueue(coro())
  raise Exception

The exception will cancel everthing scheduled in tg, but if some or all of coro() completes eagerly any side-effects of this will be observable in further execution. If tg.create_task() is used instead no part of coro() will be executed.

Pitch

At Instagram we’ve observed ~70% of coroutine instances passed to asyncio.gather() can run fully synchronously i.e. without performing any I/O which would suspend execution. This typically happens when there is a local cache which can elide actual I/O. We exploit this in Cinder with a modified asyncio.gather() that eagerly executes coroutine args and skips scheduling a asyncio.Task object to an event loop if no yield occurs. Overall this optimization saved ~4% CPU on our Django webservers.

In a prototype implementation of this proposed feature [2] the overhead when scheduling TaskGroups with all fully-synchronous coroutines was decreased by ~8x. When scheduling a mixture of synchronous and asynchronous coroutines, performance is improved by ~1.4x, and when no coroutines can complete synchronously there is still a small improvement.

We anticipate code relying on any semantics which change between TaskGroup.create_task() and TaskGroup.enqueue() will be rare. So, as the TaskGroup interface is new in 3.11, we hope enqueue() and its performance benefits can be promoted as the preferred method for scheduling coroutines in 3.12+.

Previous discussion

This new API was discussed informally at PyCon 2022, with at least some of this being between @gvanrossum, @DinoV, @markshannon, and /or @jbower-fb.

[1] The name "enqueue" came out of a discussion between @gvanrossum and @DinoV.

[2] Prototype implementation (some features missing, e.g. specifying Context), and benchmark.

Linked PRs

  • gh-97696: DRAFT asyncio eager tasks factory prototype #101613
  • gh-97696: asyncio eager tasks factory #102853
  • gh-97696 Remove unnecessary check for eager_start kwarg #104188
  • gh-97696 Add documentation for get_coro() behavior with eager tasks #104189
  • gh-97696: Remove redundant #include #104216
  • gh-97696: Use PyObject_CallMethodNoArgs and inline is_loop_running check #104255
  • gh-97696: Improve and fix documentation for asyncio eager tasks #104256
  • gh-97696: Move around and update the whatsnew entry for asyncio eager task factory #104298
  • gh-97696 Add documentation for get_coro() behavior with eager tasks #104304
Reacted by Felipe S. S. Schneider, Oleg Iarygin and Misagh
Reacted by Felipe S. S. Schneider
Reacted by Itamar Oren, Max Bernstein and Felipe S. S. Schneider
Reacted by Felipe S. S. Schneider and Timur Timerkhanov

Activity

Next
  1. jbower-fb
    added
    type-featureA feature request or enhancementA feature request or enhancement
    on Sep 30, 2022
  2. AlexWaygood
    added
    topic-asyncio
    on Sep 30, 2022
  3. AlexWaygood
    added this to asyncioon Sep 30, 2022
  4. AlexWaygood
    moved this to Todo in asyncioon Sep 30, 2022
  5. kumaraditya303
    added
    3.12only security fixesonly security fixes
    performancePerformance or resource usagePerformance or resource usage
    on Oct 1, 2022
  6. gvanrossum commented on Oct 1, 2022

    @gvanrossum
    gvanrossum
    on Oct 1, 2022
    Member
    More actions

    If cond is True and some or all of coro() completes eagerly, any side-effects of this will be observable in further execution. If tg.create_task() is used instead, no part of coro() will be executed if cond is True.

    If I didn't already understand the changes to async functions you are proposing I wouldn't understand what this means (or the code fragment above it). What does cond refer to? I think what you are trying to say here is that, if the body of the with TaskGroup() block raises before the first await, it will cancel all created tasks before they have started executing. Right?

    Separately -- Can you remind me of the reason to make this a TaskGroup method instead of a new flag on create_task()? And the enqueue() name feels odd, especially since it may immediately execute some of the coroutine, which seems the opposite of putting something in a queue.

    To be clear, I have no objection to the feature, and I think making this an opt-in part of task creation avoids worries of changing semantics for something as simple as await coro().

  7. itamaro commented on Oct 1, 2022

    @itamaro
    itamaro
    on Oct 1, 2022
    Contributor
    More actions

    Separately -- Can you remind me of the reason to make this a TaskGroup method instead of a new flag on create_task()? And the enqueue() name feels odd, especially since it may immediately execute some of the coroutine, which seems the opposite of putting something in a queue.

    This came up in the conversation we had with @DinoV during the Language Summit. We started out with a flag on create_task (e.g. eager=True), but we didn't like the fact that when setting this flag, the name of the method may now be misleading (because, in fact, it may not create a task).
    enqueue was the alternative (I don't remember who suggested it and why), but we can definitely bikeshed the naming :)
    If we take inspiration from Trio's nurseries, we can use start_soon.
    Other options are execute or run (although these names may be misleading too, since users may assume these APIs never create a task).

  8. jbower-fb commented on Oct 2, 2022

    @jbower-fb
    jbower-fb
    on Oct 2, 2022
    ContributorAuthor
    More actions

    If I didn't already understand the changes to async functions you are proposing I wouldn't understand what this means (or the code fragment above it). What does cond refer to? I think what you are trying to say here is that, if the body of the with TaskGroup() block raises before the first await, it will cancel all created tasks before they have started executing. Right?

    Right. Yeah, this is more contrived than needed, I'll fix up the text.

    This came up in the conversation we had with @DinoV during the Language Summit. We started out with a flag on create_task (e.g. eager=True), but we didn't like the fact that when setting this flag, the name of the method may now be misleading (because, in fact, it may not create a task).

    I wasn't in this conversation but when writing this issue it occurred to me that even with eager execution we could still return something Task-like. This would mitigate any issues with knowing the interface of the returned object (e.g. is cancel() available). This would also mean the name create_task() would still make sense even with a keyword-argument to alter the behavior. Unless, I'm missing something which doesn't make this viable?

    Having said this, I think eager execution should be the default behavior users reach for going forward. So it might still be better to have a new method.

  9. gvanrossum commented on Oct 2, 2022

    @gvanrossum
    gvanrossum
    on Oct 2, 2022
    Member
    More actions

    Thanks for fixing up the description, it's clear now.

    I wasn't in this conversation but when writing this issue it occurred to me that even with eager execution we could still return something Task-like. This would mitigate any issues with knowing the interface of the returned object (e.g. is cancel() available). This would also mean the name create_task() would still make sense even with a keyword-argument to alter the behavior. Unless, I'm missing something which doesn't make this viable?

    It could return a Future, which has many of the same methods. If the coroutine returns an immediate result that could be the Future's result.

    However, if the result is generally not used though the need to return something with appropriate methods would just reduce the performance benefit.

    Having said this, I think eager execution should be the default behavior users reach for going forward. So it might still be better to have a new method.

    Yeah, that is definitely an advantage of a new method name. I find enqueue() rather easy to mistype though.

    Have the performance results been repeated with TaskGroup.enqueue(), or are the quoted measurements based on the Cinder implementation using gather()? There could be surprises here.

  10. jbower-fb commented on Oct 2, 2022

    @jbower-fb
    jbower-fb
    on Oct 2, 2022
    ContributorAuthor
    More actions

    Have the performance results been repeated with TaskGroup.enqueue(), or are the quoted measurements based on the Cinder implementation using gather()? There could be surprises here.

    The benchmark showing an 8x improvement (etc.) is with Dino's prototype of TaskGroup.enqueue() against some revision of 3.11. This is functional, but still needs some work. Notably it does not yet implement management of the current Context when eagerly executing. There will be some extra overhead from that but my hope is it'll be negligible.

    It could return a Future, which has many of the same methods. If the coroutine returns an immediate result that could be the Future's result.

    However, if the result is generally not used though the need to return something with appropriate methods would just reduce the performance benefit.

    Dino's prototype currently returns a newly constructed Future if the eager execution fully completes, but returns a Task otherwise. While I can imagine it'll be less common to use the extra fields on Task, it might be annoying to have to think about whether a Future or a Task is returned. Again, unless I missed something, I don't think there should be extra overhead from returning some kind of eagerly-completed-Task value vs. a completed Future.

    I find enqueue() rather easy to mistype though.

    Indeed, I have misspelled it a number of times already. I like start_soon or start_immediate which to me imply execution start time may not be bound by the async with scope.

  11. gvanrossum commented on Oct 2, 2022

    @gvanrossum
    gvanrossum
    on Oct 2, 2022
    Member
    More actions

    For a new API, returning either a Future or a Task is totally fine -- Task is a subclass of Future, so we can just document it as returning a Future.

    I hadn't realized the prototype doesn't even need C changes -- I had assumed it depended on the changes to vectorcall to pass through a bit indicating it's being awaited immediately. Is that change even needed once we have this? (Probably we resolved that already during discussions at PyCon, it's just so long ago I can't remember anything.)

    I think we need to bikeshed some more on the name, but you're right about the impression the name ought to give. Maybe start_task()?

  12. jbower-fb commented on Oct 3, 2022

    @jbower-fb
    jbower-fb
    on Oct 3, 2022
    ContributorAuthor
    More actions

    We can just document it as returning a Future.

    I think it'd be better to document it as returning a Task? That way it's clear cancellation is still generally an option.

    I had assumed it depended on the changes to vectorcall to pass through a bit indicating it's being awaited immediately. Is that change even needed once we have this? (Probably we resolved that already during discussions at PyCon, it's just so long ago I can't remember anything.)

    That is an interesting question, and again I don't think I was around if that was discussed in detail. For this feature, that flag is not needed. If we want to add an optimization for the large body of existing code that uses asyncio.gather(), then it would help significantly. We are probably going to carry this feature forward in Cinder for a while at least as it'll likely take quite some time (years?) before we can fully migrate to the new TaskGroup API. I'd definitely like to know what your, or other members of the community feel about adding an optimization to help with the older/existing API.

    Maybe start_task()?

    Fine with me. Maybe we should discuss further once a PR is up.

  13. gvanrossum commented on Oct 3, 2022

    @gvanrossum
    gvanrossum
    on Oct 3, 2022
    Member
    More actions

    I think it'd be better to document it as returning a Task? That way it's clear cancellation is still generally an option.

    From a static type POV it's always a Future, not always a Task. If you want to cancel it you take your chances -- cancel() will return a bool telling you whether it worked. The docs should just explain the situation without lying.

    I will wait for the PR (that you're hopefully working on?). I recommend using start_task() as the method name until we come up with something better.

  14. jbower-fb commented on Oct 6, 2022

    @jbower-fb
    jbower-fb
    on Oct 6, 2022
    ContributorAuthor
    More actions

    From a static type POV it's always a Future, not always a Task.

    Whoops, I just realized Future has a cancel() method. I mistakenly thought that was a feature of Task. Sorry for the confusion.

    I will wait for the PR (that you're hopefully working on?)

    We should have something up soon. Really wanted to get an issue open first for early feedback on the plan.

  15. 51 remaining items

  16. carljm
    added a commit that references this issue on May 9, 2023

    Merge branch 'main' into inlinecomp2

    76077cd
  17. kumaraditya303
    added a commit that references this issue on May 9, 2023

    gh-97696: Move around and update the whatsnew entry for asyncio eager…

    85f9818
  18. carljm
    added a commit that references this issue on May 9, 2023

    Merge branch 'main' into inlinecomp2

    95401fe
  19. gvanrossum commented on May 9, 2023

    @gvanrossum
    gvanrossum
    on May 9, 2023
    Member
    More actions

    What’s left? Is there a what’s new entry?

  20. itamaro commented on May 9, 2023

    @itamaro
    itamaro
    on May 9, 2023
    Contributor
    More actions

    What’s left? Is there a what’s new entry?

    I think this is done now!
    Yes, we added a what's new entry.

  21. carljm
    closed this as completedon May 9, 2023
  22. github-project-automation
    moved this from Todo to Done in Release and Deferred blockers 🚫on May 9, 2023
  23. github-project-automation
    moved this from In Progress to Done in asyncioon May 9, 2023
  24. carljm
    added a commit that references this issue on May 9, 2023

    Merge branch 'main' into fixsendfamily

    a614216
  25. gvanrossum
    added a commit that references this issue on May 9, 2023

    gh-97696 Add documentation for get_coro() behavior with eager tasks (#…

    2866e03
  26. carljm
    added 3 commits that reference this issue on May 9, 2023

    Merge branch 'main' into fixsendfamily

    e40071d

    Merge branch 'main' into supermon

    c48eee3

    Merge branch 'main' into fixloadsuperspec

    cf11908
  27. bdraco
    added a commit that references this issue on Feb 25, 2024

    Add support for eager tasks

    364babc
  28. balloob
    added a commit that references this issue on Feb 26, 2024

    Add support for eager tasks (#111425)

    67e3569
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.12only security fixesonly security fixesperformancePerformance or resource usagePerformance or resource usagerelease-blockertopic-asynciotype-featureA feature request or enhancementA feature request or enhancement

    Projects

    • Release and Deferred blockers 🚫
      Status
      Done
      Show more project fields
    • asyncio
      Status
      Done
      Show more project fields

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.