Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How are async lambdas in the ForEach()-method handled?

We came across a bug in our product and reduced it to the following problem. Given a list and call to the ForEach-Extension method with an async lambda, what is the expected order of the output:

public static async Task Main()
{
    var strings = new List<string> { "B", "C", "D" };
    Console.WriteLine("A");
    strings.ForEach(async s => { await AsyncMethod(s); } );
    Console.WriteLine("E");
}

private static async Task AsyncMethod(string s)
{
    await Task.Run(() => { Console.WriteLine(s); });
}

We expected it to be always A,B,C,D,E. But sometimes it's A,B,C,E,D or A,B,E,D,C

We thought that these two lines would be equivalent:

strings.ForEach(async s => { await AsyncMethod(s); });

foreach (var s in strings) await AsyncMethod(s);

Can somebody explain where the difference is? How are these async lambdas executed and why are they not awaited?

Clarification: The problem is not the order of B,C and D. The problem is that E comes before the loop is finished

like image 449
tomfroehle Avatar asked Dec 03 '22 20:12

tomfroehle


1 Answers

foreach (var s in strings) await AsyncMethod(s);

You're misunderstanding how this works. These are the steps that are taken, sequentially:

  1. Handle "B" asynchronously.
  2. Wait for (1).
  3. Handle "C" asynchronously.
  4. Wait for (3).
  5. Handle "D" asynchronously.
  6. Wait for (5).

The await is part of each iteration. The next iteration won't start until the current one is finished.

Due to not handling the tasks asynchronously, these sequential tasks will finish in the order that they were started.


strings.ForEach(async s => { await AsyncMethod(s); });

This, on the other hand, works differently:

  1. Handle "B" asynchronously.
  2. Handle "C" asynchronously.
  3. Handle "D" asynchronously.

The ForEach starts the tasks, but does not immediately await them. Due to the nature of asynchronous handling, these concurrent tasks can be completed in a different order each time you run the code.

As there is nothing that awaits the tasks that were spawned by the ForEach, the "E" task is started immediately. BCDE are all being handled asynchronously and can be completed in any arbitrary order.


You can redesign your foreach example to match your ForEach example:

foreach (var s in strings) 
{
    AsyncMethod(s);
}

Now, the handling is the same as in the ForEach:

  1. Handle "B" asynchronously.
  2. Handle "C" asynchronously.
  3. Handle "D" asynchronously.

However, if you want to ensure that the E task is only started when BCD have all been completed, you simply await the BCD tasks together by keeping them in a collection:

foreach (var s in strings) 
{
    myTaskList.Add(AsyncMethod(s));
}

await Task.WhenAll(myTaskList);
  1. Handle "B" asynchronously and add its task to the list.
  2. Handle "C" asynchronously and add its task to the list.
  3. Handle "D" asynchronously and add its task to the list.
  4. Wait for all tasks in the list before doing anything else.
like image 170
Flater Avatar answered Dec 17 '22 00:12

Flater