首页 文章

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

提问于
浏览
75

我使用c#的 async / await 关键字(我是c#5.0的新手)遇到了一些异步编程的最佳实践 .

给出的建议之一是:

Stability: Know your synchronization contexts

...某些同步上下文是不可重入和单线程的 . 这意味着在给定时间内只能在上下文中执行一个工作单元 . 一个例子是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 中等待结果 . 同时,说数据准备好了 . 为什么't the main thread continue it'的延续逻辑并从 GetDataAsync() 返回字符串结果?

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

5 回答

  • 6

    看看here中的示例,斯蒂芬有明确的答案:

    所以这就是发生的事情,从顶级方法开始(用于ASP.NET的UI / MyController.Get的Button1_Click):顶级方法调用GetJsonAsync(在UI / ASP.NET上下文中) . GetJsonAsync通过调用HttpClient.GetStringAsync(仍在上下文中)启动REST请求 . GetStringAsync返回未完成的Task,表示REST请求未完成 . GetJsonAsync等待GetStringAsync返回的任务 . 捕获上下文,稍后将用于继续运行GetJsonAsync方法 . GetJsonAsync返回未完成的Task,表示GetJsonAsync方法未完成 . 顶级方法同步阻止GetJsonAsync返回的任务 . 这会阻止上下文线程 . ...最终,REST请求将完成 . 这样就完成了GetStringAsync返回的任务 . GetJsonAsync的延续现在可以运行了,它等待上下文可用,以便它可以在上下文中执行 . 僵局 . 顶级方法是阻塞上下文线程,等待GetJsonAsync完成,GetJsonAsync正在等待上下文空闲,以便它可以完成 . 对于UI示例,“上下文”是UI上下文;对于ASP.NET示例,“context”是ASP.NET请求上下文 . 这种类型的死锁可能会导致“上下文” .

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

    Await, and UI, and deadlocks! Oh my!

  • 0
    • 事实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() 示例的一半处 .

  • 67

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

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

    在调用之前

    • ,清除SynchronizationContext

    • 打电话,这里不再有死锁,等待它完成

    • 恢复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);
        }
    
  • 10

    另一个要点是你不应该阻止任务,并一直使用async来防止死锁 . 然后它将是所有异步而非同步阻塞 .

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

    我要解决的问题是在请求结果之前对任务使用 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);
            }
        }
    }
    

    我不足以进入该领域,看看这个解决方案的缺点(如果有的话)

相关问题