Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sequential version of Task.WhenAll

Tags:

c#

async-await

Is there a nonblocking Task.WaitAll similar to Task.WhenAll, but not parallel?

I wrote this, but maybe it’s built-in?

public async Task<IEnumerable<T>> AwaitAllAsync<T>(IEnumerable<Task<T>> tasks)
{
    List<T> result = new List<T>();
    foreach(var task in tasks)
    {
        result.Add(await task);
    }

    return result;
}

I want to know if there is a built-in way of waiting for all tasks to complete in async, but a sequential way.

Consider this code:

public class SaveFooCommandHandler : ICommandHandler<SaveFooCommand>
{
   private readonly IBusinessContext context;

   public SaveFooCommandHandler(IBusinessContext context)
   {
      this.context = context;
   }

   public async Task Handle(SaveFooCommand command)
   {
      var foos = (await Task.WhenAll(command.Foos.Select(foo => context.FindAsync<Foo>(foo.Id))).ToList()

      ...
   }
}

That will fail, but

var foos = await context.AwaitAllAsync(command.Foos.Select(foo => context.FindAsync<Foo>(foo.Id));

will not, context.FindAsync is an abstraction of dbcontext.Set<T>().FindAsync

You could do await context.Set<Foo>().Where(f => command.Foos.Contains(f.Id)).ToListAsync(), but the example is simplified.

like image 523
Anders Avatar asked May 18 '15 14:05

Anders


1 Answers

I think the core misunderstanding is around the Task type. In asynchronous code, a Task is always already running. So this doesn't make sense:

Is there a non-blocking Task.WaitAll similar to Task.WhenAll but not parallel concurrent?

If you have a collection of tasks, they're all already started.

I want to know if there is a build in way of waiting for all tasks to complete in async but sequential way.

You can, of course, await them sequentially. The standard pattern for this is to use await inside a foreach loop, just like the method you posted.

However, the only reason the sequential-await works is because your LINQ query is lazily evaluated. In particular, if you reify your task collection, it will fail. So this works:

var tasks = command.Foos.Select(foo => context.FindAsync<Foo>(foo.Id));
var foos = await context.AwaitAllAsync(tasks);

and this fails:

var tasks = command.Foos.Select(foo => context.FindAsync<Foo>(foo.Id))
    .ToList();
var foos = await context.AwaitAllAsync(tasks);

Internally, Task.WhenAll reifies your task sequence so it knows how many tasks it needs to wait for.

But this is really beside the point. The real problem you're trying to solve is how to serially execute asynchronous code, which is most easily done using foreach:

var foos = new List<Foo>();
foreach (var fooId in command.Foos.Select(f => f.Id))
  foos.Add(await context.FindAsync<Foo>(fooId));
like image 193
Stephen Cleary Avatar answered Oct 14 '22 20:10

Stephen Cleary