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

时间:2013-02-22 09:52:30

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

我使用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()的字符串结果?

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

5 个答案:

答案 0 :(得分:70)

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

  

所以这就是从顶级方法开始的情况   (Button1_Click for UI / MyController.Get for ASP.NET):

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

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

  4.   
  5. GetStringAsync返回未完成的任务,表示REST请求未完成。

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

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

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

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

  14.   
  15. 死锁。顶级方法是阻塞上下文线程,等待GetJsonAsync完成,GetJsonAsync正在等待   上下文是免费的,所以它可以完成。对于UI示例,   “context”是UI上下文;对于ASP.NET示例,“上下文”是   ASP.NET请求上下文。可以导致这种类型的死锁   “上下文”。

  16.   

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

Await, and UI, and deadlocks! Oh my!

答案 1 :(得分:14)

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

答案 2 :(得分:11)

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

答案 3 :(得分:2)

另一个要点是你不应该阻止任务,并一直使用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();
}

答案 4 :(得分:0)

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

我还不了解该解决方案的缺点(如果有)