我们在产品中遇到了一个错误,并将其减少到以下问题。 给定一个列表并使用异步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在循环结束之前出现
答案 0 :(得分:4)
foreach (var s in strings) await AsyncMethod(s);
你误解了它是如何工作的。这些是按顺序采取的步骤:
- 异步处理“B”。
- 等待(1)。
- 异步处理“C”。
- 等待(3)。
- 异步处理“D”。
- 等待(5)。
醇>
await
是每次迭代的一部分。在当前的迭代结束之前,下一次迭代不会开始。
由于没有异步处理任务,这些顺序任务将按照它们的启动顺序完成。
strings.ForEach(async s => { await AsyncMethod(s); });
另一方面,这种方式有所不同:
- 异步处理“B”。
- 异步处理“C”。
- 异步处理“D”。
醇>
ForEach
启动任务,但不会立即等待它们。由于异步处理的性质,每次运行代码时,这些并发任务可以以不同的顺序完成。
由于没有任何东西可以等待ForEach
生成的任务,因此立即启动“E”任务。 BCDE都是异步处理的,可以任意顺序完成。
您可以重新设计foreach
示例以匹配您的ForEach
示例:
foreach (var s in strings)
{
AsyncMethod(s);
}
现在,处理与ForEach
:
- 异步处理“B”。
- 异步处理“C”。
- 异步处理“D”。
醇>
但是,如果您想确保仅在BCD完成后才启动E任务,您只需将BCD任务保存在一个集合中即可等待它们:
foreach (var s in strings)
{
myTaskList.Add(AsyncMethod(s));
}
await Task.WhenAll(myTaskList);
- 异步处理“B”并将其任务添加到列表中。
- 异步处理“C”并将其任务添加到列表中。
- 异步处理“D”并将其任务添加到列表中。
- 在执行任何其他操作之前,请等待列表中的所有任务。
醇>
答案 1 :(得分:1)
问题是由于没有等待任务在列表的每个字符串上完成而引起的。
{{1}}
上述解决方案正常运行。希望它有所帮助!
答案 2 :(得分:1)
ForEach
不支持async
委托,因为它需要Action<T>
。这会将您的方法降低到无法等待的async void
。 ForEach
从未打算用于Func<Task>
或任何其他异步变体。在.Wait
上调用AsyncMethod
将导致单线程同步上下文出现死锁。你有几个选择: