ForEach() 方法中的异步 lambda 是如何处理的?

tom*_*hle 4 c# linq async-await

我们在我们的产品中遇到了一个错误,并将其简化为以下问题。给定一个列表并使用异步 lambda 调用 ForEach-Extension 方法,输出的预期顺序是什么:

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); });
}
Run Code Online (Sandbox Code Playgroud)

我们期望它总是 A、B、C、D、E。但有时是 A,B,C,E,D 或 A,B,E,D,C

我们认为这两行是等价的:

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

foreach (var s in strings) await AsyncMethod(s);
Run Code Online (Sandbox Code Playgroud)

有人能解释一下区别在哪里吗?这些异步 lambda 是如何执行的,为什么不等待它们?

澄清:问题不是B,C,D的顺序。问题是E在循环完成之前出现

Fla*_*ter 11

foreach (var s in strings) await AsyncMethod(s);
Run Code Online (Sandbox Code Playgroud)

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); });
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)
  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.


JSt*_*ard 5

ForEach不支持async委托,因为它需要Action<T>. 这会让你的方法变得不可等待async voidForEach从未打算用于Func<Task>任何其他异步变体。调用.WaitonAsyncMethod将导致单线程同步上下文死锁。不过你有几个选择: