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

我偶然发现了一些使用 c # 的 async/await关键字进行异步编程的最佳实践(我是 c # 5.0的新手)。

其中一项建议如下:

稳定性: 了解您的同步上下文

一些同步上下文是不可重入的和单线程的。这意味着在给定的时间只能在上下文中执行一个工作单元。这方面的一个例子是 WindowsUI 线程或 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();
}

If I try to dissect it myself, the main thread spawns to a new one in MyWebService.GetDataAsync();, but since the main thread awaits there, it waits on the result in GetDataAsync().Result. Meanwhile, say the data is ready. Why doesn't the main thread continue it's continuation logic and returns a string result from GetDataAsync() ?

有人能解释一下为什么上面的例子会出现僵局吗? I'm completely clueless about what the problem is ...

78028 次浏览

Take a look at this example, Stephen has a clear answer for you:

这就是从顶级方法(用于 UI 的 Button1_Click/用于 ASP.NET 的 MyController.Get)开始的过程:

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

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

  3. GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.

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

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

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

  7. The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context.

  8. 僵局。顶级方法阻塞上下文线程,等待 GetJsonAsync完成,而 GetJsonAsync等待上下文被释放以便完成。对于 UI 示例,“ context”是 UI 上下文; 对于 ASP.NET 示例,“ context”是 ASP.NET 请求上下文。这种类型的死锁可能由任何一种“上下文”引起。

另一个你应该阅读的链接: 等待,用户界面,死锁! 哦,我的天

另一个要点是您不应该阻塞 Tasks,而应该一直使用异步来防止死锁。那么它将全部是异步的非同步阻塞。

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();
}
  • Fact 1: GetDataAsync().Result; will run when the task returned by GetDataAsync() completes, in the meantime it blocks the UI thread
  • 事实2: 等待的延续(return result.ToString())被排队到 UI 线程执行
  • 事实3: 当其队列延续运行时,GetDataAsync()返回的任务将完成
  • 事实4: 因为 UI 线程被阻塞,所以队列中的延续永远不会运行(事实1)

僵局!

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

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

这在 article by Stephen Toub中解释得非常好,大约在他使用 DelayAsync()例子的中间。

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

在想要从 sync 方法调用 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);
}

我想到的一个解决办法是在询问结果之前对任务使用 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);
}
}
}

I'm not enough into the domain to see the drawbacks of this solution (if any)