Task.Run的奇怪行为

时间:2013-12-23 21:34:36

标签: c# asynchronous task-parallel-library async-await

我正在努力解开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的值,而不是它们被调用时的值?这样做的必然结果是如何正确地将本地状态传递给新任务

再一次,道歉,因为我知道代码是错误的,但它确实似乎产生了一种奇怪的行为。

1 个答案:

答案 0 :(得分:6)

  1. 您没有就await的结果致电Task.Run,或以其他方式对其进行任何操作。你把这项任务扔到了地板上。

  2. 您正在关闭循环变量。在循环内部复制i并关闭该副本。

  3. 因为你没有等待Task.Run你正在解雇然后忘记所有任务,而不是一次执行一项任务。因为您正在关闭循环变量,所以当任何实际运行的任务i被设置为3时。

    在对Task.Run的调用中编译此代码时,您将收到警告,具体表明您可能应该等待它。你也会得到doLoop没有等待任何事情的警告,尽管是异步,这是另一个迹象表明这里出了问题。

    如果您希望它们按顺序运行,那么您可以忽略循环闭包问题,只需添加await即可。如果您希望它们并行运行,则将所有任务粘贴到您可以调用await Task.WhenAll(...)的任务集合中。

    在旁注中,如果您确实希望将所有调用并行化为sleep,则根本不需要使用Task.Run。至少不会只要sleep有一个合理的异步实现。如果你只是简单地调用N次并将生成的任务粘贴到一个集合中,它们将全部异步并行运行,而不会增加要求线程池线程启动异步操作并将结果放入数组的开销。没有必要增加开销。另请注意,如果您有Task<T>的集合,那么WhenAll将为您提供Task<T[]>,因此无需将所有单个结果明确地放入数组中。但是,使用Task.Run有点低效,并且不必要地使代码复杂化,而不是导致它无法工作的原因。