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

时间:2018-02-23 10:24:12

标签: 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); });
}

我们预计它总是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);

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

澄清:问题不在于B,C和D的顺序。问题是E在循环结束之前出现

3 个答案:

答案 0 :(得分:4)

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

你误解了它是如何工作的。这些是按顺序采取的步骤:

  
      
  1. 异步处理“B”。
  2.   
  3. 等待(1)。
  4.   
  5. 异步处理“C”。
  6.   
  7. 等待(3)。
  8.   
  9. 异步处理“D”。
  10.   
  11. 等待(5)。
  12.   

await是每次迭代的一部分。在当前的迭代结束之前,下一次迭代不会开始。

由于没有异步处理任务,这些顺序任务将按照它们的启动顺序完成。

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

另一方面,这种方式有所不同:

  
      
  1. 异步处理“B”。
  2.   
  3. 异步处理“C”。
  4.   
  5. 异步处理“D”。
  6.   

ForEach启动任务,但不会立即等待它们。由于异步处理的性质,每次运行代码时,这些并发任务可以以不同的顺序完成。

由于没有任何东西可以等待ForEach生成的任务,因此立即启动“E”任务。 BCDE都是异步处理的,可以任意顺序完成。

您可以重新设计foreach示例以匹配您的ForEach示例:

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

现在,处理与ForEach

中的处理相同
  
      
  1. 异步处理“B”。
  2.   
  3. 异步处理“C”。
  4.   
  5. 异步处理“D”。
  6.   

但是,如果您想确保仅在BCD完成后才启动E任务,您只需将BCD任务保存在一个集合中即可等待它们:

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

await Task.WhenAll(myTaskList);
  
      
  1. 异步处理“B”并将其任务添加到列表中。
  2.   
  3. 异步处理“C”并将其任务添加到列表中。
  4.   
  5. 异步处理“D”并将其任务添加到列表中。
  6.   
  7. 在执行任何其他操作之前,请等待列表中的所有任务。
  8.   

答案 1 :(得分:1)

问题是由于没有等待任务在列表的每个字符串上完成而引起的。

{{1}}

上述解决方案正常运行。希望它有所帮助!

答案 2 :(得分:1)

ForEach不支持async委托,因为它需要Action<T>。这会将您的方法降低到无法等待的async voidForEach从未打算用于Func<Task>或任何其他异步变体。在.Wait上调用AsyncMethod将导致单线程同步上下文出现死锁。你有几个选择: