等待(await)和任务等待(Task.Wait) - 死锁?

270
我不太明白 Task.Wait await 之间的区别。
我有类似于以下函数的ASP.NET WebAPI服务:
public class TestController : ApiController
{
    public static async Task<string> Foo()
    {
        await Task.Delay(1).ConfigureAwait(false);
        return "";
    }

    public async static Task<string> Bar()
    {
        return await Foo();
    }

    public async static Task<string> Ros()
    {
        return await Bar();
    }

    // GET api/test
    public IEnumerable<string> Get()
    {
        Task.WaitAll(Enumerable.Range(0, 10).Select(x => Ros()).ToArray());

        return new string[] { "value1", "value2" }; // This will never execute
    }
}

Get会发生死锁。

是什么原因导致了这种情况?如果我使用阻塞等待而不是await Task.Delay,为什么这不会引起问题呢?


3
Task.Delay(1).Wait() 基本上与 Thread.Sleep(1000) 是完全相同的。在实际的生产代码中,它很少是合适的选择。 - Servy
1
@ronag:你的WaitAll引起了死锁。请查看我回答中的博客链接获取更多详细信息。你应该使用await Task.WhenAll代替。 - Stephen Cleary
我不太明白我正在按照你提供的链接中所示使用 ConfigureAwait(false)。我不能一路使用异步,因为那将需要在我的实际代码中进行太多的代码更改。 - ronag
10
由于您使用了 ConfigureAwait(false),所以单个对 BarRos 的调用不会死锁,但是,由于您使用了一个可枚举类型,它会创建多个任务并等待所有这些任务完成,因此第一个任务将死锁第二个任务。如果您改用 await Task.WhenAll 来等待所有任务完成,以避免阻塞 ASP 上下文,您将看到该方法正常返回。 - Servy
2
@ronag 另一个选项是在整个树上添加 .ConfigureAwait(false),直到阻塞,这样就不会再尝试返回到主上下文; 这将起作用。另一种选择是启动内部同步上下文。链接。如果你将 Task.WhenAll 放入 AsyncPump.Run 中,它将有效地阻塞整个过程,而无需在任何地方使用 ConfigureAwait, 但这可能是一个过于复杂的解决方案。 - Servy
显示剩余3条评论
3个回答

379

Waitawait - 虽然在概念上类似,但它们实际上是完全不同的。

Wait 会阻塞当前线程,直到任务完成。因此,当前线程会被完全阻塞,等待任务完成。一般来说,你应该沿着 "async" 的方向继续下去;也就是说,不要对 async 代码进行阻塞。在我的博客中,我详细介绍了 在异步代码中进行阻塞会导致死锁的原因

await 会异步等待任务完成。这意味着当前的方法被 "暂停"(其状态被捕获),并且该方法返回一个未完成的任务给其调用者。后来,当 await 表达式完成时,剩余的方法将被安排为继续执行。

你还提到了“协作式阻塞”,我想你指的是你正在 Wait 上的任务可能会在线程上执行。在某些情况下,这可能发生,但这是一种优化。有许多情况下它是做不到的,例如任务属于另一个调度程序、任务已经开始或者是非代码任务(例如在你的代码示例中:Wait 无法内联执行 Delay 任务,因为没有代码可供它执行)。

你可能会发现我的 async/await 入门 对你有所帮助。


9
我认为有一个误解,“等待”(Wait)可以正常工作,但“等待异步操作完成”(await)会死锁。 - ronag
6
不,任务调度程序不会这样做。Wait 阻塞了线程,并且不能用于其他事情。 - Stephen Cleary
9
我猜你混淆了你的方法名称,导致死锁是由于阻塞代码引起的,并且使用 await 代码可以解决。或者死锁与两者都无关,你误诊了问题。 - Servy
4
这是有意设计的——在 UI 应用中非常有效,但对于 ASP.NET 应用程序来说可能会成为障碍。ASP.NET Core 通过移除“同步上下文”来解决了这个问题,因此在 ASP.NET Core 请求中阻塞不再导致死锁。 - Stephen Cleary
3
@StephenCleary 这太神奇了。我来这里是因为我在阅读《C#并发编程实战》一书时遇到了一些理解困难的地方,其中你首次提到死锁问题,而现在我意识到你就是这本书的作者。 - adamasan
显示剩余19条评论

31

根据我从不同的来源阅读到的内容:

await 表达式不会阻塞正在执行它的线程。相反,它会导致编译器将剩余的 async 方法注册为等待任务完成后的续体。控制权然后返回给 async 方法的调用者。当任务完成时,它会调用其续体,并恢复 async 方法的执行状态。

要等待单个task 完成,您可以调用它的 Task.Wait 方法。对 Wait 方法的调用将阻塞调用线程,直到单个类实例完成执行。无参数的 Wait() 方法用于无条件等待任务完成。该任务通过调用 Thread.Sleep 方法模拟工作,休眠两秒钟。

这篇文章也是一个很好的阅读材料。


3
那这不是技术上不正确吗?有人可以澄清一下吗? - 我可以澄清一下,您是在问这个问题吗?(我只想明确您是在提问还是回答)。如果您是在提问:这可能会更好地作为一个单独的问题;作为回答,它不太可能在这里收集到新的回复。 - Marc Gravell
1
我已经回答了这个问题,并在这里提出了一个疑问 https://stackoverflow.com/questions/53654006/using-task-wait-instead-of-await-for-async-programming 感谢@MarcGravell。现在,您能否请取消对答案的删除投票? - Ayushmati
请问您能否现在撤销对答案的删除投票?那不是我的答案;感谢♦,任何我这样的投票都会立即生效。然而,我认为这个答案并没有回答问题的关键点,即死锁行为。 - Marc Gravell
1
这是不正确的。除非第一个await被达到,否则一切都会被阻塞。 - user1785960
@user1785960,尽管您说的是对的,但这并不意味着答案没有用处。 - Soner from The Ottoman Empire
使用async await在技术上并不会创建一个线程,而是会导致你的代码共享线程,与Task/Thread相反。Async用于不阻塞UI,而线程用于非UI阻塞逻辑。 - Nick Turner

6

其他答案没有提供一些重要的事实:

async/await 在 CIL 级别上更加复杂,因此会消耗内存和 CPU 时间。

如果等待时间不可接受,则可以取消任何任务。

在使用 async/await 的情况下,我们没有一个处理程序来取消或监视这样的任务。

使用 Task 比 async/await 更加灵活。

任何 sync 功能都可以被包装成 async

public async Task<ActionResult> DoAsync(long id) 
{ 
    return await Task.Run(() => { return DoSync(id); } ); 
} 
async/await会带来许多问题。我们不知道是否会在运行时和上下文调试时到达await语句。如果第一个await没有到达,则一切都被阻塞。有时即使await似乎已到达,仍然一切都被阻塞:https://github.com/dotnet/runtime/issues/36063 我不明白为什么必须使用代码重复的syncasync方法或使用技巧。
结论:手动创建任务并控制它们更好。处理程序(Handler)对任务(Task)提供更多控制。我们可以监视和管理任务:https://github.com/lsmolinski/MonitoredQueueBackgroundWorkItem 抱歉我的英语。

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