ganchito55
ganchito55

Reputation: 3607

ForEach lambda async vs Task.WhenAll

I have an async method like this:

private async Task SendAsync(string text) {
  ...
}

I also have to use this method one time for each item in a List:

List<string> textsToSend = new Service().GetMessages();

Currently my implementation is this:

List<string> textsToSend = new Service().GetMessages();
List<Task> tasks = new List<Task>(textsToSend.Count);
textsToSend.ForEach(t => tasks.Add(SendAsync(t)));
await Task.WhenAll(tasks);

With this code, I get a Task for each message that runs async the sending method.

However, I don't know if is there any different between my implementation and this one:

List<string> textsToSend = new Service().GetMessages();
textsToSend.ForEach(async t => await SendAsync(t));

In the second one, I don't have the List<Task> allocation, but I think that the first one launch all Task in parallel and the second sample, one by one.

Could you help me to clarify if is there any different between the first and second samples?

PD: I also know that C#8 supports foreach async, however I'm using C# 7

Upvotes: 3

Views: 3003

Answers (2)

Panagiotis Kanavos
Panagiotis Kanavos

Reputation: 131374

You don't even need a list, much less ForEach to execute multiple tasks and await all of them. In any case, ForEach is just a convenience function that uses `foreach.

To execute some async calls concurrently bases on a list of inputs all you need is Enumerable.Select. To await all of them to complete you only need Task.WhenAll :

var tasks=textsToSend.Select(text=>SendAsync(text));
await Task.WhenAll(tasks);

LINQ and IEnumerable in general use lazy evaluation which means Select's code won't be executed until the returned IEnumerable is iterated. In this case it doesn't matter because it's iterated in the very next line. If one wanted to force all tasks to start a call to ToArray() would be enough, eg :

var tasks=textsToSend.Select(SendAsync).ToArray();

If you wanted to execute those async calls sequentially, ie one after the other, you could use a simple foreach. There's no need for C# 8's await foreach :

foreach(var text in textsToSend)
{
    await SendAsync(text);
}

The Bug

This line is simply a bug :

textsToSend.ForEach(async t => await SendAsync(t));

ForEach doesn't know anything about tasks so it never awaits for the generated tasks to complete. In fact, the tasks can't be awaited at all. The async t syntax creates an async void delegate. It's equivalent to :

async void MyMethod(string t)
{
    await SendAsync(t);
}

textToSend.ForEach(t=>MyMethod(t));

This brings all the problems of async void methods. Since the application knows nothing about those async void calls, it could easily terminate before those methods complete, resulting in NREs, ObjectDisposedExceptions and other weird problems.

For reference check David Fowler's Implicit async void delegates

C# 8 and await foreach

C# 8's IAsyncEnumerable would be useful in the sequential case, if we wanted to return the results of each async operation in an iterator, as soon as we got them.

Before C# 8 there would be no way to avoid awaiting for all results, even with sequential execution. We'd have to collect all of them in a list. Assuming each operation returned a string, we'd have to write :

async Task<List<string> SendTexts(IEnumerable<string> textsToSend)
{
    var results=new List<string>();
    foreach(var text in textsToSend)
    {
        var result=await SendAsync(text);
        results.Add(result);
    }
}

And use it with :

var results=await SendTexts(texts);

In C# 8 we can return individual results and use them asynchronously. We don't need to cache the results before returning them either :

async IAsyncEmumerable<string> SendTexts(IEnumerable<string> textsToSend)
{
    foreach(var text in textsToSend)
    {
        var result=await SendAsync(text);
        yield return;
    }
}


await foreach(var result in SendTexts(texts))
{
   ...
}

await foreach is only needed to consume the IAsyncEnumerable result, not produce it

Upvotes: 7

AlbertK
AlbertK

Reputation: 13187

that the first one launch all Task in parallel

Correct. And await Task.WhenAll(tasks); waits for all messages are sent.

The second one also sends messages in parallel but doesn't wait for all messages are sent since you don't await any task.

In your case:

textsToSend.ForEach(async t => await SendAsync(t));

is equivalent to

textsToSend.ForEach(t => SendAsync(t));

the async t => await SendAsync(t) delegate may return the task (it depends on assignable type) as SendAsync(t). In case of passing it to ForEach both async t => await SendAsync(t) and SendAsync(t) will be translated to Action<string>.

Also the first code will throw an exception if any SendAsync throws an excepion. In the second code any exception will be ignored.

Upvotes: 1

Related Questions