【问题标题】:An async/await example that causes a deadlock导致死锁的异步/等待示例
【发布时间】:2021-09-18 07:35:33
【问题描述】:

我遇到了一些使用 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()返回一个字符串结果?

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

【问题讨论】:

  • 你真的确定 GetDataAsync 完成了它的工作吗?或者它卡住了导致只是锁定而不是死锁?
  • 这是提供的示例。据我了解,它应该完成它的工作并准备好某种结果......
  • 为什么还要等任务呢?你应该等待,因为你基本上失去了异步模型的所有好处。
  • 补充@ToniPetrina 的观点,即使没有死锁问题,var data = GetDataAsync().Result; 是一行代码,应该永远在你不应该的上下文中完成块(UI 或 ASP.NET 请求)。 即使它没有死锁,它也会在不确定的时间内阻塞线程。 所以基本上这是一个糟糕的例子。 [你需要在执行这样的代码之前离开 UI 线程,或者像 Toni 建议的那样在那里使用await。]

标签: c# task-parallel-library deadlock async-await c#-5.0


【解决方案1】:

看看this example,斯蒂芬给你一个明确的答案:

这就是发生的事情,从顶级方法开始(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 请求上下文。这种类型的死锁可能由任一“上下文”引起。

您应该阅读的另一个链接:Await, and UI, and deadlocks! Oh my!

【讨论】:

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

    死锁!

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

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

    这在article by Stephen Toub 中得到了很好的解释,大约在他使用DelayAsync() 示例的一半处。

    【讨论】:

    • 关于var data = Task.Run(GetDataAsync).Result,这对我来说是新的。我一直认为,只要GetDataAsync 的第一个等待被击中,外部.Result 就会随时可用,所以data 将永远是default。有趣。
    【解决方案3】:

    我只是在 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);
    }
    

    【讨论】:

      【解决方案4】:

      另一个要点是你不应该阻塞任务,并且一直使用异步来防止死锁。那么这将是所有异步而不是同步阻塞。

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

      【讨论】:

      • 如果我想要主 (UI) 线程在任务完成之前被阻塞怎么办?或者例如在控制台应用程序中?假设我想使用支持异步的HttpClient...我如何同步使用它没有死锁的风险?这必须是可能的。如果 WebClient 可以这样使用(因为有同步方法)并且可以完美运行,那么为什么不能使用 HttpClient 来完成呢?
      • 请参阅上面 Philip Ngan 的回答(我知道这是在此评论之后发布的):将等待的延续排队到未阻塞的不同线程,例如使用 var data = Task.Run(GetDataAsync).Result
      • @Dexter - re "如果我希望主 (UI) 线程在任务完成之前被阻塞怎么办?" - 你真的想要阻塞 UI 线程吗?用户不能做任何事情,甚至不能取消 - 还是你不想继续你正在使用的方法? "await" 或 "Task.ContinueWith" 处理后一种情况。
      • @ToolmakerSteve 我当然不想继续这个方法。但是我只是不能使用await,因为我也不能一直使用异步-在main中调用了HttpClient,这当然不能是异步的。然后我提到在控制台应用程序中执行所有这些 - 在这种情况下,我想要的正是前者 - 我不希望我的应用程序甚至 多线程。阻止一切
      【解决方案5】:

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

      代码如下所示:

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

      join方法在哪里:

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

      我对域的了解还不够,无法看到此解决方案的缺点(如果有的话)

      【讨论】:

      • 我喜欢这个,调度动作在 Redux 中效果很好。我正在学习 c# 并想知道,这有什么问题,它有效吗?为什么这被否决了?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-22
      相关资源
      最近更新 更多