Skip to content

[API Proposal]: Making "Process asynchronous tasks as they complete" easy by using IAsyncEnumerable  #61959

Closed

Description

@Vijay-Nirmal

EDITED on 1/23/2024 by @stephentoub:

namespace System.Threading.Tasks;

public class Task
{
+   public static IAsyncEnumerable<Task> WhenEach(params Task[] tasks);
+   public static IAsyncEnumerable<Task> WhenEach(params ReadOnlySpan<Task> tasks); // params when possible
+   public static IAsyncEnumerable<Task> WhenEach(IEnumerable<Task> tasks);

+   public static IAsyncEnumerable<Task<TResult>> WhenEach(params Task<TResult>[] tasks); // params for now; move it to ReadOnlySpan overload when that syntax is possible
+   public static IAsyncEnumerable<Task<TResult>> WhenEach(params ReadOnlySpan<Task<TResult>> tasks); // params when possible
+   public static IAsyncEnumerable<Task<TResult>> WhenEach(IEnumerable<Task<TResult>> tasks);
}

Background and motivation

Currently, if we need to "Process asynchronous tasks as they complete" then we need to write lots of unnecessary codes and its not straight forward, something like below.

// Using currently available APIs
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
while (downloadTasks.Any())
{
    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    downloadTasks.Remove(finishedTask);
    Process(await finishedTask);
}

API Proposal

namespace System.Threading.Tasks
{
    public class Task : IAsyncResult, IDisposable
    {
        public static IAsyncEnumerable<Task> WhenEach(params Task[] tasks);  // Please change the name, if needed
        public static IAsyncEnumerable<Task> WhenEach(IEnumerable<Task> tasks);
        public static IAsyncEnumerable<Task<TResult>> WhenEach(params Task<TResult>[] tasks); 
        public static IAsyncEnumerable<Task<TResult>> WhenEach(IEnumerable<Task<TResult>> tasks);
    }
}

API Usage

// Using newly created APIs
await foreach (var finishedTask in Task.WhenEach(downloadTasksQuery))
{
    Process(await finishedTask);
}

Alternative Designs

No response

Risks

No response

Updates

(Others can edit this section and add more info)

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Nov 23, 2021
  2. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Nov 23, 2021
  3. svick commented on Nov 23, 2021

    @svick
    Member

    This method already exists in AsyncEx, where it's called OrderByCompletion. It returns a (non-async) collection of wrapper tasks, but I suspect that's because it's older than IAsyncEnumerable<T> and using IAsyncEnumerable<T> returning the original tasks is the better approach today.

  4. added this to the Future milestone on Nov 24, 2021
  5. added
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Nov 24, 2021
  6. theodorzoulias commented on Nov 25, 2021

    @theodorzoulias
    Contributor

    @Vijay-Nirmal a name that might be more suitable for the Task.WhenAnyAsEnumerable method is Task.WhenEach. :-)

  7. sakno commented on Dec 10, 2021

    @sakno
    Contributor

    You can achieve the same behavior with TaskCompletionPipe that has native support of IAsyncEnumerable<T> as well as channel-like methods WaitToReadAsync and TryRead.

  8. modified the milestones: Future, 9.0.0 on Jan 23, 2024
  9. self-assigned this
    on Jan 23, 2024
  10. added
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    and removed
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Jan 23, 2024
  11. bartonjs commented on Mar 26, 2024

    @bartonjs
    Member

    Video

    Looks good as proposed

    namespace System.Threading.Tasks;
    
    public class Task
    {
        public static IAsyncEnumerable<Task> WhenEach(params Task[] tasks);
        public static IAsyncEnumerable<Task> WhenEach(params ReadOnlySpan<Task> tasks);
        public static IAsyncEnumerable<Task> WhenEach(IEnumerable<Task> tasks);
    
        public static IAsyncEnumerable<Task<TResult>> WhenEach(params Task<TResult>[] tasks);
        public static IAsyncEnumerable<Task<TResult>> WhenEach(params ReadOnlySpan<Task<TResult>> tasks);
        public static IAsyncEnumerable<Task<TResult>> WhenEach(IEnumerable<Task<TResult>> tasks);
    }
  12. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Mar 26, 2024
  13. added
    in-prThere is an active PR which will close this issue when it is merged
    on Mar 26, 2024
  14. dahlbyk commented on Mar 31, 2024

    @dahlbyk
    Contributor

    Nit: missing <TResult>:

    +   public static IAsyncEnumerable<Task<TResult>> WhenEach<TResult>(params Task<TResult>[] tasks); // params for now; move it to ReadOnlySpan overload when that syntax is possible
    +   public static IAsyncEnumerable<Task<TResult>> WhenEach<TResult>(params ReadOnlySpan<Task<TResult>> tasks); // params when possible
    +   public static IAsyncEnumerable<Task<TResult>> WhenEach<TResult>(IEnumerable<Task<TResult>> tasks);
  15. alrz commented on Apr 1, 2024

    @alrz
    Member

    @svick

    using IAsyncEnumerable<T> returning the original tasks is the better approach today.

    I wonder what is the benefit here. Wouldn't IEnumerable be cheaper?

    At first look I thought this is going to return the value instead of the task because we're awaiting that one on MoveNextAsync.

    public static IAsyncEnumerable<TResult> WhenEach<TResult>(IEnumerable<Task<TResult>> tasks)
  16. stephentoub commented on Apr 1, 2024

    @stephentoub
    Member

    I wonder what is the benefit here. Wouldn't IEnumerable be cheaper?

    You'd block synchronously in MoveNext waiting for the next task to complete.

    At first look I thought this is going to return the value instead of the task because we're awaiting that one on MoveNextAsync.

    It's returning the completed task, just like with WhenAny. That gives the consumer the ability to examine / use the completed Task however they like.

  17. alrz commented on Apr 1, 2024

    @alrz
    Member

    @stephentoub

    You'd block synchronously in MoveNext waiting for the next task to complete.

    If I'm not mistaken, OrderByCompletion returns a IEnumerable<Task<T>> which you're indeed be awaiting to examine / use however you'd like. Note there's no blocking -- MoveNext returns immediately since we're enumerating an array.

    It's returning the completed task, just like with WhenAny. That gives the consumer the ability to examine / use the completed Task however they like.

    So what I'm saying is that IEnumerable<Task<T>> will give you exactly that, IAsyncEnumerable seems like an auxiliary helper when you want to stop on first exception.

    IAsyncEnumerable<T> WhenEach(Task<T>[] tasks) {
      foreach (Task<T> task in tasks.OrderByCompletion())
        yield return await task;
      }
    }      
  18. stephentoub commented on Apr 1, 2024

    @stephentoub
    Member

    @alrz, how would you propose for MoveNext to immediately complete such that Current could return the next Task even when no next task had yet completed? The only way to do that would be to allocate a new Task that could be returned immediately, with a different object identity than the original task, and then whenever the next task completes, marshal its results/exception/cancellation information to that proxy. At that point, you've significantly increased the cost and you've lost the ability to compare Tasks by reference.

    I'm not understanding the aversion to using IAsyncEnumerable here.

  19. locked and limited conversation to collaborators on May 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Threading.Tasksin-prThere is an active PR which will close this issue when it is merged

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions