我正在努力解开C#异步编程,下面的模式显然是错误的,但是我不明白。我打算做的就是解雇n个任务,然后等待它们全部完成。当我明白我感到困惑时,我开始将事情提炼下来,最终创造了这个:
private async Task<bool> doLoop()
{
int loopCnt = 3;
int[] sleeperReturns = new int[loopCnt + 1];
for (int i = 0; i < loopCnt; i++)
{
System.Diagnostics.Debug.WriteLine("doLoop: i={0} calling Sleeper", i);
Task.Run(async () => { sleeperReturns[i] = await sleep(i); });
System.Diagnostics.Debug.WriteLine("doLoop: i={0} called Sleeper", i);
}
return true;
}
private async Task<int> sleep(int i)
{
System.Diagnostics.Debug.WriteLine("Sleep: begins i={0}", i);
await Task.Delay(5000 + i * 1000);
System.Diagnostics.Debug.WriteLine("Sleep: ends i={0}", i);
return i;
}
这会产生以下输出:
doLoop: i=0 calling Sleeper
doLoop: i=0 called Sleeper
doLoop: i=1 calling Sleeper
doLoop: i=1 called Sleeper
doLoop: i=2 calling Sleeper
doLoop: i=2 called Sleeper
Sleep: begins i=3
Sleep: begins i=3
Sleep: ends i=3
Sleep: ends i=3
Sleep: ends i=3
所以,我的谜团是:
(1)为什么在doLoop完成之前没有任何睡眠任务开始?
(2)为什么,当它们被调用并且当它们返回时,它们是否从循环的结尾获得i的值,而不是它们被调用时的值?这样做的必然结果是如何正确地将本地状态传递给新任务
再一次,道歉,因为我知道代码是错误的,但它确实似乎产生了一种奇怪的行为。
答案 0 :(得分:6)
您没有就await
的结果致电Task.Run
,或以其他方式对其进行任何操作。你把这项任务扔到了地板上。
您正在关闭循环变量。在循环内部复制i
并关闭该副本。
因为你没有等待Task.Run
你正在解雇然后忘记所有任务,而不是一次执行一项任务。因为您正在关闭循环变量,所以当任何实际运行的任务i
被设置为3时。
在对Task.Run
的调用中编译此代码时,您将收到警告,具体表明您可能应该等待它。你也会得到doLoop
没有等待任何事情的警告,尽管是异步,这是另一个迹象表明这里出了问题。
如果您希望它们按顺序运行,那么您可以忽略循环闭包问题,只需添加await
即可。如果您希望它们并行运行,则将所有任务粘贴到您可以调用await Task.WhenAll(...)
的任务集合中。
在旁注中,如果您确实希望将所有调用并行化为sleep
,则根本不需要使用Task.Run
。至少不会只要sleep
有一个合理的异步实现。如果你只是简单地调用N次并将生成的任务粘贴到一个集合中,它们将全部异步并行运行,而不会增加要求线程池线程启动异步操作并将结果放入数组的开销。没有必要增加开销。另请注意,如果您有Task<T>
的集合,那么WhenAll
将为您提供Task<T[]>
,因此无需将所有单个结果明确地放入数组中。但是,使用Task.Run
有点低效,并且不必要地使代码复杂化,而不是导致它无法工作的原因。