Async-Await 在预期死锁的地方没有死锁
Async-Await no deadlock where a deadlock is expected
众所周知,同步等待异步方法会导致死锁
(参见,例如 Don't Block on Async Code)
我在事件处理程序中有以下代码,用于在 Windows Forms 应用程序中单击按钮(即调用代码时安装了 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 异步方法,如 SendAsync
或 ReadAsByteArrayAsync
似乎不会死锁。有人可以解释这种行为吗?
.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;
,我确实遇到了死锁。
Don't implementations of async methods in .NET libraries use await statements internally?
一般不会。我还没有在 .NET 框架中看到在内部使用 async-await 的单个实现。它确实使用任务和延续,但不使用 async
和 await
关键字带来的编译器魔法。
使用 async-await 很简单,因为代码看起来是同步的,但实际上是异步运行的。但这种简单性在性能上的代价是非常小。
对于大多数消费者来说,这个价格是值得的,但框架本身会尽可能地提高性能。
However, synchronously waiting on library async methods like SendAsync
or ReadAsByteArrayAsync
seems not to deadlock.
死锁是等待的默认行为的结果。当你等待一个未完成的任务时,SynchronizationContext
被捕获,当它完成时,继续在 SynchronizationContext
(如果存在)上恢复。当没有async、await、captured SynchronizationContext
等时,这种死锁是不会发生的。
HttpClient.SendAsync
专门使用 TaskCompletionSource
到 return 任务,而不将该方法标记为异步。您可以在 github here.
上的实现中看到
为异步等待添加到现有 类 的大多数任务-returning 方法只是使用现有的异步 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 图书馆)。
您不会通过阻塞 大多数 开箱即用的 Task
-返回 .NET 调用而导致死锁,因为它们不会在内部触及SynchronizationContext
启动 Task
除非绝对必要(两个原因:性能和避免死锁)。
这意味着即使标准的 .NET 调用 did 在幕后使用 async/await
(i3arnon 说他们没有 - 我不会争论我根本不知道),毫无疑问,他们会使用 ConfigureAwait(false)
除非绝对需要捕获上下文。
但这就是 .NET 框架。至于您自己的代码,如果您在客户端调用 wrapperMethod().Wait()
(或 Result
),您 将 观察到死锁(假设您是 运行 非空 SynchronizationContext.Current
- 如果您使用 Windows 表单,肯定会出现这种情况)。为什么?因为你炫耀 async/await
最佳实践 而不是 在不与 [=37= 交互的 async
方法中使用 ConfigureAwait(false)
], 导致状态机生成不必要地在原始 SynchronizationContext
.
上执行的延续
众所周知,同步等待异步方法会导致死锁 (参见,例如 Don't Block on Async Code)
我在事件处理程序中有以下代码,用于在 Windows Forms 应用程序中单击按钮(即调用代码时安装了 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 异步方法,如 SendAsync
或 ReadAsByteArrayAsync
似乎不会死锁。有人可以解释这种行为吗?
.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;
,我确实遇到了死锁。
Don't implementations of async methods in .NET libraries use await statements internally?
一般不会。我还没有在 .NET 框架中看到在内部使用 async-await 的单个实现。它确实使用任务和延续,但不使用 async
和 await
关键字带来的编译器魔法。
使用 async-await 很简单,因为代码看起来是同步的,但实际上是异步运行的。但这种简单性在性能上的代价是非常小。
对于大多数消费者来说,这个价格是值得的,但框架本身会尽可能地提高性能。
However, synchronously waiting on library async methods like
SendAsync
orReadAsByteArrayAsync
seems not to deadlock.
死锁是等待的默认行为的结果。当你等待一个未完成的任务时,SynchronizationContext
被捕获,当它完成时,继续在 SynchronizationContext
(如果存在)上恢复。当没有async、await、captured SynchronizationContext
等时,这种死锁是不会发生的。
HttpClient.SendAsync
专门使用 TaskCompletionSource
到 return 任务,而不将该方法标记为异步。您可以在 github here.
为异步等待添加到现有 类 的大多数任务-returning 方法只是使用现有的异步 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 图书馆)。
您不会通过阻塞 大多数 开箱即用的 Task
-返回 .NET 调用而导致死锁,因为它们不会在内部触及SynchronizationContext
启动 Task
除非绝对必要(两个原因:性能和避免死锁)。
这意味着即使标准的 .NET 调用 did 在幕后使用 async/await
(i3arnon 说他们没有 - 我不会争论我根本不知道),毫无疑问,他们会使用 ConfigureAwait(false)
除非绝对需要捕获上下文。
但这就是 .NET 框架。至于您自己的代码,如果您在客户端调用 wrapperMethod().Wait()
(或 Result
),您 将 观察到死锁(假设您是 运行 非空 SynchronizationContext.Current
- 如果您使用 Windows 表单,肯定会出现这种情况)。为什么?因为你炫耀 async/await
最佳实践 而不是 在不与 [=37= 交互的 async
方法中使用 ConfigureAwait(false)
], 导致状态机生成不必要地在原始 SynchronizationContext
.