导致死锁的异步/等待示例

Dro*_*iss 87 c# deadlock task-parallel-library async-await c#-5.0

我发现了一些使用c#async/ awaitkeywords的异步编程的最佳实践(我是c#5.0的新手).

给出的建议之一是:

稳定性:了解同步上下文

...某些同步上下文是不可重入和单线程的.这意味着在给定时间内只能在上下文中执行一个工作单元.一个例子是Windows UI线程或ASP.NET请求上下文.在这些单线程同步上下文中,很容易使自己陷入僵局.如果从单线程上下文中生成任务,然后在上下文中等待该任务,则等待代码可能会阻止后台任务.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}
Run Code Online (Sandbox Code Playgroud)

如果我自己尝试剖析它,主线程会产生一个新线程MyWebService.GetDataAsync();,但由于主线程在那里等待,它等待结果GetDataAsync().Result.同时,说数据准备好了.为什么主线程不继续它的延续逻辑并从中返回字符串结果GetDataAsync()

有人可以解释一下为什么上面的例子中存在死锁吗?我完全不知道问题是什么......

cuo*_*gle 75

看看这里的例子,斯蒂芬有明确的答案:

所以这就是发生的事情,从顶级方法开始(用于ASP.NET的UI/MyController.Get的Button1_Click):

  1. 顶级方法调用GetJsonAsync(在UI/ASP.NET上下文中).

  2. GetJsonAsync通过调用HttpClient.GetStringAsync(仍在上下文中)启动REST请求.

  3. GetStringAsync返回未完成的Task,表示REST请求未完成.

  4. GetJsonAsync等待GetStringAsync返回的任务.捕获上下文,稍后将用于继续运行GetJsonAsync方法.GetJsonAsync返回未完成的Task,表示GetJsonAsync方法未完成.

  5. 顶级方法同步阻止GetJsonAsync返回的任务.这会阻止上下文线程.

  6. ...最终,REST请求将完成.这样就完成了GetStringAsync返回的任务.

  7. GetJsonAsync的延续现在可以运行了,它等待上下文可用,以便它可以在上下文中执行.

  8. 僵局.顶级方法是阻塞上下文线程,等待GetJsonAsync完成,GetJsonAsync正在等待上下文空闲,以便它可以完成.对于UI示例,"上下文"是UI上下文; 对于ASP.NET示例,"context"是ASP.NET请求上下文.这种类型的死锁可能会导致"上下文".

您应该阅读的另一个链接:

等待,用户界面和死锁!天啊!


Phi*_*gan 15

  • 事实1:GetDataAsync().Result;将在返回的任务GetDataAsync()完成时运行,同时它会阻止UI线程
  • 事实2:await(return result.ToString())的延续排队到UI线程执行
  • 事实3:返回的任务GetDataAsync()将在其排队的延续运行时完成
  • 事实4:排队的延续永远不会运行,因为UI线程被阻止(事实1)

僵局!

可以通过提供的替代方案来打破僵局,以避免事实1或事实2.

  • 避免1,4.使用var data = await GetDataAsync(),它允许UI线程继续运行,而不是阻止UI线程
  • 避免2,3.将await的继续排队到一个未被阻止的不同线程,例如use var data = Task.Run(GetDataAsync).Result,它将继续发布到线程池线程的同步上下文.这允许返回的任务 GetDataAsync()完成.

这在Stephen Toub一篇文章中得到了很好的解释,大约在他使用的例子的一半DelayAsync().


小智 13

我只是在一个MVC.Net项目中再次摆弄这个问题.如果要从PartialView调用异步方法,则不允许使PartialView异步.如果你这样做,你会得到一个例外.

因此,在您想要从同步方法调用异步方法的场景中,基本上是一个简单的解决方法,您可以执行以下操作:

  1. 在调用之前,清除SynchronizationContext
  2. 做电话,这里不再有僵局,等待它完成
  3. 恢复SynchronizationContext

例:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}
Run Code Online (Sandbox Code Playgroud)