使用 await实现异步 API时要使用的一件关键事情是确保在 API impl 中等待任务时使用ConfigureAwait(false) 。这样做是允许 TPL 使用 TPL 默认行为(线程池)而不是 TaskAwaiter 的默认行为(当前同步上下文)来安排您的等待恢复。
使用当前同步上下文对于消费者来说是正确的默认行为,因为如果您已经在 UI 线程上,它允许诸如 await 返回到 UI 线程之类的事情。但是,如果 UI 线程无法执行该方法的其余部分,则尝试返回 UI 线程可能会出现问题。让await
线程执行该方法的方式是在底层创建委托的标准 .NET 约定。然后将这些委托发送到任何类型的调度机制(例如 WinForms 消息泵、WPF 调度程序或其他任何东西)中进行处理。
然而,对于 API 实现来说,试图回到相同的上下文通常是错误的,因为这隐含地依赖于可以执行的原始上下文。
例如,如果我在 UI 线程上有一些代码:
void MyUIThreadCode() {
Task asyncTask = MyAsyncMethod();
asyncTask.Wait();
}
async Task MyAsyncMethod() {
await DownloadSomethingAsync();
ComputeSomethingElse();
}
这种代码写起来[b]非常[/b],而且很容易导致挂起。典型的情况是,在内部MyAsyncMethod()
,有一个使用默认同步上下文调度的 await。这意味着在 UI 上下文中,将调用 DownloadSomethingAsync() 方法,然后开始下载。
MyAsyncMethod()
然后测试await
操作数是否“完成”。假设它没有完成下载,那么定义的行为await
就是剥离方法的“其余部分”,并在操作数真正完成后安排await
执行。
所以......执行该方法其余部分的状态被隐藏在委托中,现在MyAsyncMethod()
将其自己的任务返回给MyUIThreadCode()
.
现在MyUIThreadCode()
调用Task.Wait()
返回的任务。但问题是,Task
在 .NET 中,它实际上是任何具有“完成性”概念的通用表示。仅仅因为你有一个Task
对象,就无法保证它会如何执行,也无法保证它会如何完成。如果您猜对了,另一件不能保证的事情是它的隐式依赖关系。
所以在上面的例子中,MyAsyncMethod()
在一个任务上使用了默认的等待行为,它在当前上下文中调度方法继续。方法延续需要在MyAsyncMethod()
返回的任务被认为完成之前执行。
但是,MyUIThreadCode()
调用Wait()
了任务。定义的行为是阻塞当前线程,将当前函数保留在堆栈上,并有效地等待任务完成。
用户在这里没有意识到的是,他们被阻塞的任务依赖于 UI 线程主动处理,它无法做到这一点,因为它仍在忙于执行Wait()
调用阻塞的函数。
- 为了让 MyUIThreadCode() 完成它的方法调用,它需要
Wait()
返回 (2)
- 为了让 Wait() 返回,它需要 asyncTask 完成 (3)。
- 为了使 asyncTask 完成,它需要执行方法 continuation
MyAsyncMethod()
(4)。
- 为了让方法继续运行,它需要处理消息循环 (5)。
- 为了让消息循环继续处理,它需要 MyUIThreadCode() 返回 (1)。
阐明了循环依赖关系,最终没有一个条件得到满足,并且 UI 线程实际上挂起。
以下是使用 ConfigureAwait(false) 修复它的方法:
void MyUIThreadCode() {
Task asyncTask = MyAsyncMethod();
asyncTask.Wait();
}
async Task MyAsyncMethod() {
await DownloadSomethingAsync().ConfigureAwait(false);
ComputeSomethingElse();
}
这里发生的是方法延续MyAsyncMethod()
使用 TPL 默认(线程池),而不是当前同步上下文。这是现在的条件,具有这种行为:
- 为了让 MyUIThreadCode() 完成它的方法调用,它需要
Wait()
返回 (2)
- 为了让 Wait() 返回,它需要 asyncTask 完成 (3)。
- 为了使 asyncTask 完成,它需要执行方法 continuation
MyAsyncMethod()
(4)。
- (新)为了使方法继续运行,它需要线程池正在处理(5)。
- 实际上,线程池始终在处理。事实上,.NET 线程池非常适合具有高可伸缩性(动态分配和停用线程)以及低延迟(它有一个最大阈值,它允许请求在继续并开始之前变得陈旧一个新线程以确保保留吞吐量)。
您正在押注 .NET 已经成为一个可靠的平台,我们非常重视 .NET 中的线程池。
所以,有人可能会问,问题可能出在Wait()
调用上……为什么他们一开始就使用阻塞等待?
答案是有时你真的不得不这样做。例如,Main() 方法的 .NET 约定是程序在 Main() 方法返回时终止。或者...换句话说,Main() 方法会阻塞,直到您的程序完成。
接口契约或虚拟方法契约等其他事物通常具有特定的承诺,即在该方法返回之前执行某些事情。除非您的接口或虚拟方法返回一个任务......您可能需要对任何被调用的异步 API 进行一些阻止。这有效地破坏了在那种情况下异步的目的......但也许你会从不同代码路径中的异步中受益。
因此,对于返回异步任务的 API 提供者,通过使用 ConfigureAwait(false),您可以帮助确保您返回的任务没有任何意外的隐式依赖项(例如,UI 消息循环仍在主动泵送)。您可以包含的依赖项越多,您的 API 就越好。
希望这可以帮助!