【问题标题】:Deadlocked with async - not standard deadlock异步死锁 - 不是标准死锁
【发布时间】:2018-05-24 10:00:47
【问题描述】:

我称之为异步方法和死锁。现在我不知道为什么会这样。我不认为这是一个标准的死锁,因为我在 Main 方法中调用 Task.Result - 而不是在 UI 线程中。更重要的是,我还尝试使用 async Main 而不调用 Task。但结果是一样的。

问题在于 WinForms 项目。一切都从 Main 方法开始:

bool res = Task.Run(() => contr.RegisterPremiseAllInOne()).Result;

这是在新任务中对异步方法 RegisterPremiseAllInOne 的简单调用。

接下来会发生什么... RegisterPremiseAllInOne 的工作方式更像这样(简化模型):

public async Task<bool> RegisterPremiseAllInOne()
{
   AppUser loggedAppUser = await appUserContr.GetLoggedAppUser(); // <-- everything works fine here
   if(loggedAppUser == null)
      return false;

   var premises = await GetPremisesForLoggedUser(); //at the end there is a deadlock
}

我现在将向您展示每个方法的调用做了什么(一切都以对 WebApi 的其余查询结束):

GetLoggedAppUser -> 
await API.Users.GetLoggedAppUser() -> 
await ClientHelper.GetObjectFromRequest<Appuser>("AppUsers/logged") -> 
await SendAsyncRequest() -> 
await HttpClient.SendAsync()

我希望这很容易阅读。

这条路径运行良好。有趣的是,GetPremisesForLoggedUserGetLoggedAppUser 部分收敛,看起来像这样:

GetPremisesForLoggedUser -> 
await API.Premises.GetForLoggedUser() -> 
await ClientHelper.GetListFromRequest<premise>("premises/logged") -> 
await SendAsyncRequest() -> 
await HttpClient.SendAsync()

如您所见,有一条笔直的路径。调用 API 并最终发送一个 http 请求。

死锁在SendAsyncHttpClient 内。我不知道为什么。然而,第一条路径工作正常。

在服务器端一切正常。服务器返回成功响应。但是客户端挂了。

我认为它应该有效。我不会将同步代码与异步代码混在一起。所有方法都返回Task&lt;T&gt; 并标记为异步。

也许有人知道问题可能出在哪里?也许你会想看一些调试窗口的截图:Parallel Stack / Tasks?

【问题讨论】:

  • 除非您明确知道自己在做什么,否则不要在任务中使用.Result.Wait。如果您的第一行代码来自程序主代码,那么对于较新的 C# 版本,您可以使此方法也异步并改用 await。
  • 这是标准的死锁问题。您对异步调用 (Task.Run(() =&gt; contr.RegisterPremiseAllInOne())) 的阻塞 (.Result)
  • 好吧,我尝试了没有 Task.Result 的 async Main,结果是一样的。更重要的是我不在 UI 线程中调用 Result 所以它应该可以工作(并且它在不同的调用之前工作了几行)
  • 对我来说,not 看起来像标准死锁吗。结果本身并不危险。标准死锁的出现是因为任务主体试图进入正在使用的同步上下文。 Task.Run 在没有同步上下文的情况下运行。该模式通常是安全的,如果无法真正修复,通常是标准死锁的推荐解决方法。

标签: c# asynchronous deadlock


【解决方案1】:

好的,感谢@usr 和@Evk,我发现了这个错误。然而,它是标准的 UI 死锁,但没有更多的伪装。

就在我打电话给GetPremisesForLoggedUser() 之前,我正在使用工厂(IoC 容器)创建一个表单:

IPremisePickerView view = objFactory.Resolve<IPremisePickerView>();
var premises = await GetPremisesForLoggedUser();

创建表单时,该表单所属的线程自动成为 UI 线程。我认为这是在 Form 类中完成的。

原来是我在打电话 等待 GetPremisesForLoggedUser()

在 UI 线程上。所以可能所有的任务都开始在 UI 线程上运行(从某个点)。但这是否意味着 Main 中调用的 Task.Run.Result 阻塞了等待结果的 UI 线程?我不确定,因为这是在主线程上调用的(在这种情况下不是 UI 线程)。因此,如果有人可以弥补我的答案,那就太好了。

无论如何,在将表单创建移到对异步方法的调用下方之后,一切都开始工作了。

【讨论】:

  • 很高兴你发现了这个错误。似乎值得注意的是,使用 Task.Run 和 SyncContext.Current 断言的系统测试是找到它的关键。调试时应该少盯着屏幕,多做一些调查和试验。
猜你喜欢
  • 2014-09-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-13
  • 1970-01-01
  • 2020-01-18
  • 1970-01-01
  • 2014-12-09
相关资源
最近更新 更多