【问题标题】:Async-Await no deadlock where a deadlock is expectedAsync-Await 在预期出现死锁的地方没有死锁
【发布时间】:2015-12-26 10:39:12
【问题描述】:

众所周知,异步方法上的同步等待会导致死锁 (例如,参见Don't Block on Async Code

我在 Windows 窗体应用程序中单击按钮的事件处理程序中有以下代码(即调用代码时安装了 UI SynchronizationContext)。

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("http://www.google.com"));
Task<HttpResponseMessage> t = client.SendAsync(request);
t.Wait();
var response = t.Result;

我完全预料到代码会在单击按钮时死锁。但是,我实际看到的是同步等待——对话框有一段时间没有响应,然后像往常一样接受事件。 当我尝试同步等待 client 异步方法时,我总是看到死锁。但是,同步等待 library 异步方法(如 SendAsyncReadAsByteArrayAsync)似乎不会死锁。有人可以解释这种行为吗?

.NET 库中异步方法的实现是否在内部使用 await 语句,因此必须将延续编组回原始 SynchronizationContext?

注意: 如果我定义一个客户端方法,说

public async Task<byte[]> wrapperMethod()
{
    var client = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Get, new Uri("http://www.google.com"));
    var response = await client.SendAsync(request);
    return await response.Content.ReadAsByteArrayAsync();
}

然后在按钮单击处理程序中说byte[] byteArray = wrapperMethod().Result;,我确实遇到了死锁。

【问题讨论】:

  • 正如接受的答案所描述的那样,没有死锁,因为大多数 Microsoft .NET 代码不会在捕获的上下文中恢复。但是,根据这一点要小心。特别是,我相信HttpClient 在某些移动平台上确实使用了await(并且 - 错误地 - 在捕获的上下文中恢复)。

标签: c# asynchronous async-await deadlock synchronizationcontext


【解决方案1】:

.NET 库中异步方法的实现是否在内部使用 await 语句?

一般来说,不会。我还没有在 .NET 框架中看到一个在内部使用 async-await 的实现。它确实使用了任务和延续,但没有使用 asyncawait 关键字带来的编译器魔法。

使用 async-await 很简单,因为代码看起来是同步的,但实际上是异步运行的。但这种简单性在性能上的代价是非常小

对于大多数消费者来说,这个价格是值得付出的,但框架本身会尽可能地提高性能。

但是,同步等待库异步方法(如 SendAsyncReadAsByteArrayAsync)似乎不会死锁。

死锁是 await 的默认行为的结果。当您等待未完成的任务时,SynchronizationContext 被捕获,当它完成时,继续在该 SynchronizationContext(如果存在)上恢复。当没有async、await、捕获SynchronizationContext等时,这种死锁是不会发生的。

HttpClient.SendAsync 专门使用TaskCompletionSource 来返回任务,而不将方法标记为异步。你可以在github上的实现中看到here

为 async-await 添加到现有类的大多数任务返回方法只是使用现有的异步 API(即BeginXXX/EndXXX)构建任务。例如这是TcpClient.ConnectAsync

public Task ConnectAsync(IPAddress address, int port)
{
    return Task.Factory.FromAsync(BeginConnect, EndConnect, address, port, null);
}

当您确实使用 async-await 时,虽然您在不需要捕获 SynchronizationContext 时使用 ConfigureAwait(false) 避免了死锁。建议库应始终使用它,除非需要上下文(例如 UI 库)。

【讨论】:

  • 我认为“性能”部分在创建新库时使用 async/await 的上下文中非常具有误导性。 BCL 是否使用async/await?可能不是。 在创建新库时应该使用async/await吗?确实。以下是微软“.NET 4.5 中的 TPL 性能改进”中的一句话:“对新的“await”关键字的支持建立在任务延续之上,但 await 支持经过高度优化,通常比“现成的“续篇”
  • 关于Task在.NET 4.5中引入的单延续优化以及对async/await的影响的完整论文可以在这里找到:blogs.msdn.com/b/pfxteam/archive/2011/11/10/10235962.aspx
  • 澄清:我试图理解为什么同步等待库方法似乎没有死锁,而不是如何避免这种死锁。关于第一个问题的任何想法 - 为什么同步等待上面指出的库方法不会死锁?
  • @Anirudh,当您责备SynchronizationContext 而不是async/await 时,您实际上是在做某事,因为最终导致僵局的是SynchronizationContext 回发,无论使用何种高级技术.但是,我想说将上下文捕获为异步HttpClient 调用的一部分是常识。只有Tasks 必须 与他们启动的线程交互,这将在您的复制中开箱即用地死锁,并且您不会在 .NET 中找到很多这样的基类库(我只能想到异步Dispatcher 调用)。
  • @i3arnon,顺便感谢ValueTask 的信息 - 这是我今天学到的非常令人兴奋的东西。
【解决方案2】:

阻塞大多数开箱即用的Task-returning .NET 调用不会导致死锁,因为它们不会在内部触及SynchronizationContext 而@987654325除非绝对必要,否则会启动 @(两个原因:性能和避免死锁)。

这意味着即使标准 .NET 调用确实在幕后使用 async/await(i3arnon 说他们没有 - 我不会争论,因为我根本不知道) ,毫无疑问,他们会使用ConfigureAwait(false),除非绝对需要捕获上下文。

但这就是 .NET 框架。至于您自己的代码,如果您在客户端调用wrapperMethod().Wait()(或Result),您观察到死锁(前提是您使用非空SynchronizationContext.Current 运行 - 如果您使用的是 Windows 窗体,这肯定是这种情况)。为什么?因为您在不与 UI 交互的async 方法内的等待对象上使用ConfigureAwait(false) 炫耀async/await 最佳实践,从而导致状态机生成不必要地执行的延续原SynchronizationContext

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多