await vs Task.Wait - 死锁?

Posted

技术标签:

【中文标题】await vs Task.Wait - 死锁?【英文标题】:await vs Task.Wait - Deadlock? 【发布时间】:2012-10-30 13:54:08 【问题描述】:

我不太明白Task.Waitawait 之间的区别。

我在 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 时,为什么这不会导致问题?

【问题讨论】:

@Servy:我一有时间就会带回一个仓库。目前它适用于Task.Delay(1).Wait(),这已经足够好了。 Task.Delay(1).Wait() 基本上与Thread.Sleep(1000) 完全相同。在实际的生产代码中,它很少适用。 @ronag:您的WaitAll 导致了死锁。有关更多详细信息,请参阅我的答案中指向我的博客的链接。你应该改用await Task.WhenAll @ronag 因为你有 ConfigureAwait(false) 一个 single 调用 BarRos 不会死锁,但因为你有一个可枚举的创建超过一个然后等待所有这些,第一个栏将死锁第二个。如果您 await Task.WhenAll 而不是等待所有任务,这样您就不会阻塞 ASP 上下文,您将看到该方法正常返回。 @ronag 你的另一个选择是添加.ConfigureAwait(false) 一直向上直到你阻塞,这样就没有什么永远试图回到主要背景;那行得通。另一种选择是启动内部同步上下文。 Link。如果您将Task.WhenAll 放在AsyncPump.Run 中,它将有效地阻止整个事情,而您无需在任何地方使用ConfigureAwait,但这可能是一个过于复杂的解决方案。 【参考方案1】:

Waitawait - 虽然概念上相似 - 但实际上完全不同。

Wait 将同步阻塞,直到任务完成。因此,当前线程实际上被阻塞,等待任务完成。作为一般规则,您应该使用“async一直向下”;也就是说,不要阻塞async 代码。在我的博客上,我详细介绍了how blocking in asynchronous code causes deadlock。

await 将异步等待任务完成。这意味着当前 方法 处于“暂停”状态(它的状态被捕获)并且该方法向其调用者返回一个不完整的任务。稍后,当await 表达式完成时,该方法的其余部分被安排为一个延续。

您还提到了“合作块”,我假设您的意思是您正在 Waiting 执行的任务可能在等待线程上执行。在某些情况下可能会发生这种情况,但这是一种优化。在很多情况下它不能发生,例如任务是针对另一个调度程序,或者它已经启动或者它是非代码任务(例如在您的代码示例中:@987654331 @ 不能内联执行 Delay 任务,因为它没有代码)。

您可能会发现我的async / await intro 很有帮助。

【讨论】:

我认为有一个误解,Wait 工作正常await 死锁。 很明显:是的,如果我用Task.Delay(1).Wait() 替换我的await Task.Delay(1),服务就可以正常工作,否则就会死锁。 不,任务调度程序不会这样做。 Wait阻塞线程,不能用于其他事情。 @ronag 我猜你只是把你的方法名弄混了,你的死锁实际上是由阻塞代码引起的,并且与await 代码一起工作。要么,要么死锁与任何一个都无关,你误诊了问题。 @hexterminator:这是设计使然 - 它适用于 UI 应用程序,但往往会妨碍 ASP.NET 应用程序。 ASP.NET Core 已通过删除 SynchronizationContext 来解决此问题,因此 ASP.NET Core 请求中的阻塞不再出现死锁。【参考方案2】:

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

await 表达式不会阻塞正在执行它的线程。相反,它会导致编译器注册 async 方法的其余部分作为等待任务的延续。然后控制权返回给async 方法的调用者。当任务完成时,它会调用它的继续,async 方法的执行会从它停止的地方继续。

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

This article 也不错。

【讨论】:

“那在技术上不是不正确吗?有人可以澄清一下吗?” - 我可以澄清一下吗?你是在问这个问题吗? (我只是想清楚你是在问还是在回答)。如果您要问:作为一个单独的问题可能会更好;不太可能在这里收集新的回复作为答案 我已经回答了这个问题,并针对我在这里的疑问提出了一个单独的问题***.com/questions/53654006/… 谢谢@MarcGravell。您现在可以删除您对答案的删除投票吗? "您现在可以删除您对答案的删除投票吗?" ——那不是我的;多亏了♦,我的任何此类投票都会立即生效。但是,我不认为这回答了问题的关键点,即关于死锁行为。 这不是真的。直到第一次等待未达到所有内容都被阻止 @user1785960 虽然你是对的,但这并不意味着答案是无用的。【参考方案3】:

其他答案中没有给出一些重要的事实:

“异步等待”在 CIL 级别更复杂,因此会消耗内存和 CPU 时间。

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

在“异步等待”的情况下,我们没有处理此类任务的处理程序来取消或监控它。

使用 Task 比“async await”更灵活。

任何同步功能都可以被异步包装。

public async Task<ActionResult> DoAsync(long id) 
 
    return await Task.Run(() =>  return DoSync(id);  ); 
 

“async await”会产生很多问题。我们现在不是在没有运行时和上下文调试的情况下会到达 await 语句。 如果未达到第一次等待,则所有内容都会被阻止。有时甚至等待似乎到达仍然一切都被阻止:

https://github.com/dotnet/runtime/issues/36063

我不明白为什么我必须忍受同步和异步方法或使用黑客的代码重复。

结论:手动创建任务并控制它们要好得多。 Task 的处理程序给予更多的控制。我们可以监控任务并管理它们:

https://github.com/lsmolinski/MonitoredQueueBackgroundWorkItem

对不起我的英语。

【讨论】:

以上是关于await vs Task.Wait - 死锁?的主要内容,如果未能解决你的问题,请参考以下文章

是 .GetAwaiter().GetResult();一般使用安全吗?

在 C# 中使用 Web 服务:删除 Task.Wait()

使用Task.Wait而不是等待异步编程

使用async await完成: 等到task线程内任务结束 交给主线程处理事情 task线程内不会出现卡死现象

谈谈Tomcat占用cpu高的问题

[译]async/await中使用阻塞式代码导致死锁