调用异步方法中的 .Wait() 和 Task.Run().Wait() 的区别

3
我在一个ASP.NET WebForms网站上有服务器端的点击事件。在此事件中,我调用一个方法,该方法又调用其异步伙伴方法,并在调用上添加了.Wait()
然后,这个方法会进一步调用多层异步方法(例如,调用另一个异步方法,该方法又调用另一个异步方法,以此类推),最终在HttpClient对象上调用异步方法。此时,线程似乎消失在了兔子洞里;该方法从未回调。
现在,我知道异步调用序列按预期工作,因为相同的代码也从Web API控制器调用(控制器方法调用第一个方法的异步版本,而不是同步的“伙伴”方法),它按预期返回。
所以基本上我有像这样的东西,但它永远不会返回。
protected void btn_Click(object sender, EventArgs e)
    > Class1.DoSomething()
        > Class1.DoSomethingAsync.Wait()
            ...
                > await ClassN.Authenticate()
                  {
                    await myHttpClient.PostAsync()  // never returns
                  }

我确实尝试在第一个异步方法中使用.ConfigureAwait(false),但没有成功。

我还有这个,它确实返回:

Task<IHttpActionResult> MyWebApiMethod()
    > await Class1.DoSomethingAsync()
        ...
            > await ClassN.Authenticate()
              {
                await myHttpClient.PostAsync()  // does return
              }

我发现如果将第一个版本改为以下内容,它就可以工作:
protected void btn_Click(object sender, EventArgs e)
    > Class1.DoSomething()
        > Task.Run(async () => await Class1.DoSomethingAsync()).Wait()
            ...
                > await ClassN.Authenticate()
                  {
                    await myHttpClient.PostAsync()
                  }

但我不知道为什么。
有人能解释一下调用之间的区别吗?
Class1.DoSomethingAsync.Wait()

并调用
Task.Run(async () => await Class1.DoSomethingAsync()).Wait()

3
为什么不按照正确的方式使用它,即使用async void btn_Clickawait Class1.DoSomethingAsync() - Zein Makki
1
不要在已经是异步方法的情况下使用 Task.Run,这样会浪费线程。只需按照 @user3185569 建议的更改 button_click 事件处理程序的签名即可。 - Fabio
@user3185569 - 我之前没有使用过async在服务器端的WebForms事件上,因为我不知道我可以这样做。谢谢你指出来给我。 - awj
1个回答

8
我在我的博客文章《不要阻塞异步代码》和我的MSDN文章《异步最佳实践》中解释了这种行为。
线程似乎陷入了一个兔子洞,方法从未回调。
这是因为其中一个await试图在ASP.NET上下文中恢复,但请求线程(使用该ASP.NET上下文)正在阻塞等待任务完成。这就是导致死锁的原因。
我确实尝试在第一个异步方法上使用.ConfigureAwait(false),但没有成功。
为了避免使用ConfigureAwait(false)时出现死锁,必须将其应用于每个调用的方法中的每个await。因此,DoSomethingAsync必须对每个await使用它,DoSomethingAsync调用的每个方法必须对每个await使用它(例如,Authenticate),那些方法调用的每个方法必须对每个await使用它(例如,PostAsync),等等。请注意,在此结束时,您依赖于库代码,并且实际上HttpClient在过去错过了其中的一些。
我发现如果我更改它(使用Task.Run),第一个版本可以正常工作。
是的。 Task.Run将在没有任何上下文的线程池线程上执行其委托。因此,这就是为什么没有死锁的原因:没有await尝试在ASP.NET上下文中恢复。
为什么不以正确的方式使用它?async void btn_Click和await Class1.DoSomethingAsync()?
不要在已经异步的代码上使用Task.Run,这是浪费线程。只需更改button_click事件处理程序的签名即可。
这里是正确的答案:不要阻塞异步代码。只需使用async void事件处理程序并使用await即可。
P.S. ASP.NET Core不再具有ASP.NET上下文,因此您可以尽情阻塞而不必担心死锁。但是,当然,您仍然不应该这样做,因为这是低效的。

我在“正确”答案上部分持不同意见……有时候你需要从普通的异步代码中调用异步方法,这种代码使用线程、锁和信号量,或者在一个长堆栈的深处,你不想让它感染到所有的根节点,因为那会带来“async Task<return>”病毒。 - Louis Somers
@LouisSomers:有一些技巧可用于从同步代码调用异步代码,但没有适用于所有情况的解决方案。我认为你评论中的关键是“不想”——几乎总是有正确的方法来做到这一点;只是人们不(或更常见的是业务不会优先考虑这项工作)。 - Stephen Cleary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接