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

本文关键字:等待 异步 死锁 | 更新日期: 2023-09-27 18:00:27

我遇到了一些使用 c# 的 async/await 关键字进行异步编程的最佳实践(我是 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();
}

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

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

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

看看这个例子,Stephen 给你一个明确的答案:

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

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

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

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

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

  5. 顶级方法在 GetJsonAsync 返回的Task上同步阻塞。这会阻止上下文线程。

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

  7. GetJsonAsync 的延续现在已准备好运行,它会等待上下文可用,以便可以在上下文中执行。

  8. 死锁。顶级方法是阻止上下文线程,等待GetJsonAsync完成,GetJsonAsync正在等待上下文空闲以便它可以完成。对于 UI 示例,"上下文"是 UI 上下文;对于 ASP.NET 示例,"上下文"是 ASP.NET 请求上下文。这种类型的死锁可能是由任一"上下文"引起的。

您应该阅读的另一个链接:等待,UI,以及死锁!天呐!

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

僵局!

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

  • 修复1:避免1,4。不要阻止 UI 线程,而是使用 var data = await GetDataAsync() ,这允许 UI 线程继续运行
  • 修复2:避免2,3。将 await 的延续排队到未被阻塞的其他线程,例如使用 var data = Task.Run(GetDataAsync).Result ,这会将延续发布到线程池线程的同步上下文中。这允许GetDataAsync()返回的任务完成。

这在斯蒂芬·图布(Stephen Toub(的一篇文章中得到了很好的解释,大约在一半的地方,他使用了DelayAsync()的例子。

我只是在一个 ASP.NET MVC项目中再次摆弄这个问题。当您想从PartialView调用async方法时,不允许进行PartialView async。如果这样做,您将获得例外。

在要从同步方法调用async方法的情况下,可以使用以下简单的解决方法:

  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);
}

另一个要点是,您不应该阻止任务,并一直使用异步以防止死锁。然后它将是全异步而不是同步阻塞。

public async Task<ActionResult> ActionAsync()
{
    var data = await GetDataAsync();
    return View(data);
}
private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

我想要的一个解决方法是在请求结果之前对任务使用Join扩展方法。

代码如下所示:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;
  return View(data);
}

其中连接方法为:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

我没有足够的领域来看到这个解决方案的缺点(如果有的话(