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


94

我遇到了一些使用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完成了它的工作吗?还是卡住而导致锁定而不是死锁?
Andrey

这是提供的示例。据我了解,它应该完成它的工作并且已经准备好某种结果了……
Dror Weiss 2013年

4
您为什么还要等待任务?您应该等待,因为您基本上失去了异步模型的所有好处。
Toni Petrina

要添加到@ToniPetrina的要点,即使没有死锁问题,var data = GetDataAsync().Result;也是一行代码,在禁止阻塞的上下文(UI或ASP.NET请求)中,绝对不能这样做。即使没有死锁,它也会在不确定的时间内阻塞线程。 所以基本上是一个可怕的例子。[您需要在执行此类代码之前先退出UI线程,或者await像Toni所建议的那样在其中使用它。]
ToolmakerSteve18年

Answers:


81

看一下这个例子,Stephen为您提供了一个清晰的答案:

因此,这是从顶级方法(Button1_Click对于UI / MyController.Get对于ASP.NET)开始的事情:

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

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

  3. GetStringAsync返回未完成Task,指示REST请求未完成。

  4. GetJsonAsync等待的Task归还GetStringAsync。上下文被捕获,以后将用于继续运行该GetJsonAsync方法。GetJsonAsync返回uncompleted Task,指示该GetJsonAsync方法不完整。

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

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

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

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

您应该阅读另一个链接:等待,UI和死锁!天啊!


20
  • 事实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。将等待的继续排队到未阻塞的其他线程中,例如use var data = Task.Run(GetDataAsync).Result,它将继续继续发布到线程池线程的同步上下文中。这样就可以完成返回的任务 GetDataAsync()

这是一个在解释真的很好斯蒂芬Toub的文章,大约一半时,他使用的例子DelayAsync()


关于,var data = Task.Run(GetDataAsync).Result这对我来说是新的。我一直以为,.Result只要等到第一次等待,外部就可以随时使用GetDataAsync,所以data总是如此default。有趣。
nawfal

18

我只是在ASP.NET MVC项目中再次摆弄这个问题。当您想从调用async方法时PartialView,则不允许创建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);
}

3

另一个要点是,您不应阻塞Tasks,并一直使用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();
}

6
如果我希望在任务完成之前阻塞主(UI)怎么办?还是在控制台应用程序中?假设我要使用支持异步的HttpClient ...如何同步使用它而没有死锁的风险?这一定是可能的。如果WebClient可以那样使用(因为具有同步方法)并且可以完美地工作,那么为什么也不能使用HttpClient来完成呢?
德克斯特(Dexter)

请参阅上面的Philip Ngan的回答(我知道这是在此评论后发布的内容):将等待的继续排队到未阻塞的其他线程,例如,使用var data = Task.Run(GetDataAsync).Result
Jeroen

@Dexter-re“ 如果我要在任务完成之前阻止主(UI)线程怎么办 ”-您是否真的希望UI线程被阻止,这意味着用户无法执行任何操作,甚至无法取消-还是是否不想继续使用您所使用的方法?“ await”或“ Task.ContinueWith”处理后一种情况。
制造商史蒂夫(Steve)

@ToolmakerSteve当然我不想继续该方法。但是我根本不能使用await,因为我也不能一直使用异步-在main中调用HttpClient ,当然不能异步。然后我提到了在控制台应用程序中进行所有操作-在这种情况下,我只希望使用前者-我什至不希望我的应用程序多线程的。阻止一切
德克斯特

-1

我要解决的问题是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);
        }
    }
}

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.