导致死锁的异步/等待示例
Posted
技术标签:
【中文标题】导致死锁的异步/等待示例【英文标题】: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
。]
【参考方案1】:
看看this example,斯蒂芬给你一个明确的答案:
这就是发生的事情,从***方法开始(
Button1_Click
用于 UI /MyController.Get
用于 ASP.NET):
***方法调用
GetJsonAsync
(在 UI/ASP.NET 上下文中)。
GetJsonAsync
通过调用HttpClient.GetStringAsync
(仍在上下文中)启动 REST 请求。
GetStringAsync
返回未完成的Task
,表示REST请求未完成。
GetJsonAsync
等待由GetStringAsync
返回的Task
。上下文被捕获并将用于稍后继续运行GetJsonAsync
方法。GetJsonAsync
返回一个未完成的Task
,表示GetJsonAsync
方法没有完成。***方法在
GetJsonAsync
返回的Task
上同步阻塞。这会阻塞上下文线程。... 最终,REST 请求将完成。这完成了由
GetStringAsync
返回的Task
。
GetJsonAsync
的延续现在已准备好运行,它等待上下文可用,以便在上下文中执行。死锁。***方法正在阻塞上下文线程,等待
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
方法的场景中使用以下简单的解决方法:
-
通话前,清除
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);
【讨论】:
【参考方案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# 并想知道,这有什么问题,它有效吗?为什么这被否决了?以上是关于导致死锁的异步/等待示例的主要内容,如果未能解决你的问题,请参考以下文章