为什么在这种情况下 Task.Delay 不起作用?

23

我正在测试 async,并发现了这种我无法理解的情况:

var watch = Stopwatch.StartNew();

var t1 = Task.Factory.StartNew(async () =>
{
    await Task.Delay(2000);

    return 2;
});

var t2 = Task.Factory.StartNew(() =>
{
    Task.Delay(1000);

    return 1; 
});

await Task.WhenAll(t1, t2);

var result = watch.ElapsedMilliseconds;

我想了解为什么结果总是0!为什么不是1000、2000或两个任务的总和3000?为什么Task.WhenAll没有等待任务完成?


@GrantThomas 我正在尝试将它赋值给一个变量,但它给出了编译器错误,我该怎么做? - MuriloKunze
@MuriloKunze 要回答那个问题,我们需要看到代码行和错误信息。 - Servy
我尝试了这个:var taskresult = await Task.WhenAll(t1, t2); 但它给了我一个“无法将void分配给隐式类型的本地变量”的错误。 - MuriloKunze
1个回答

47

好的,那么第二个任务比较简单,让我们处理它。

对于第二个任务,t2,您不会对 Task.Delay(1000) 的结果执行任何操作。您不会使用 await,也不会使用 Wait 等待。鉴于该方法不是 async,我认为您想要进行阻塞等待。为此,您需要在 Delay 调用的末尾添加 Wait() 使其成为阻塞等待,或者只是使用 Thread.Sleep()


对于第一个任务,您正在受到使用 var 的事实的影响。当您不使用 var 时,发生的情况更清晰:

Task<Task<int>> t1 = Task.Factory.StartNew(async () =>
{
    await Task.Delay(2000);

    return 2;
});

你返回的是一个任务嵌套的int,而不只是一个Task 且外部任务会在内部任务启动后立即"完成"。当你使用WhenAll时,你不关心外部任务何时完成,而是关心内部任务何时完成。有很多处理方法,其中之一是使用Unwrap

Task<int> t1 = Task.Factory.StartNew(async () =>
{
    await Task.Delay(2000);

    return 2;
}).Unwrap();

现在你有了预期的 Task<int>,并且 WhenAll 将至少需要 2000 毫秒,就像预期的一样。你也可以添加另一个 await 调用来完成相同的操作:

Task<int> t1 = await Task.Factory.StartNew(async () =>
{
    await Task.Delay(2000);

    return 2;
});

正如svick在评论中提到的,另一个选择是仅使用Task.Run而不是StartNewTask.Run有一组特殊的重载方法,用于接受一个Func<Task<T>>并返回一个Task<T>对象,并自动为您取消包装:

Task<int> t1 = Task.Run(async () =>
{
    await Task.Delay(2000);

    return 2;
});

因此,当您创建异步 Lambda 时,最好使用 Task.Run 作为默认选项,因为它会为您“处理”此问题,但在无法使用 Task.Run 的复杂情况下,最好意识到这一点。


最后我们来讨论你没有采取的选项,这实际上是你应该在这种情况下采取的选项。由于 Task.Delay 已经返回了一个任务,因此没有必要首先将其放入 StartNew 中。您可以不必将其包装起来,而是直接使用它:

var t3 = Task.Delay(3000);
await Task.WhenAll(t1, t2, t3);

如果你只是想等待一段固定的时间,那么你应该这样做。


4
还有另一个选择:使用Task.Run()。它自己解包任务,因此与使用StartNew()Unwrap()相比,它可能是更好的选项。 - svick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接