原文地址Stephen Cleary
写得很详细,尤其讲到了 GUI 上下文调用,在APS.NET中它会阻塞 GUI 线程,从而导致死锁。而控制台中却不存在这个问题。
比如开发过程中本地写控制台程序测试没问题,但是发布到IIS异步处理就会出现数据库上下文方面异常。
本文只重点介绍一些淹没在文档海洋中的最佳做法。
图 1 中总结了这些指导原则;我将在以下各节中逐一讨论。
图 1 异步编程指导原则总结
| “名称” | 说明 | 异常 |
| 避免 Async Void | 最好使用 async Task 方法而不是 async void 方法 | 事件处理程序 |
| 始终使用 Async | 不要混合阻塞式代码和异步代码 | 控制台 main 方法 |
| 配置上下文 | 尽可能使用 ConfigureAwait(false) | 需要上下文的方法 |
避免 Async Void
下面的代码段演示了一个返回 void 的同步方法及其等效的异步方法:
void MyMethod() { // Do synchronous work. Thread.Sleep(1000); } async Task MyMethodAsync() { // Do asynchronous work. await Task.Delay(1000); }
但是,async void 方法的一些语义与 async Task 或 async Task<T> 方法的语义略有不同。
图 2 演示本质上无法捕获从 async void 方法引发的异常。
图 2 无法使用 Catch 捕获来自 Async Void 方法的异常
private async void ThrowExceptionAsync() { throw new InvalidOperationException(); } public void AsyncVoidExceptions_CannotBeCaughtByCatch() { try { ThrowExceptionAsync(); } catch (Exception) { // The exception is never caught here! throw; } }
可以通过对 GUI/ASP.NET 应用程序使用 AppDomain.UnhandledException 或类似的全部捕获事件观察到这些异常,但是使用这些事件进行常规异常处理会导致无法维护。
Async void 方法会在启动和结束时通知 SynchronizationContext,但是对于常规应用程序代码而言,自定义 SynchronizationContext 是一种复杂的解决方案。
可以安装 SynchronizationContext 来检测所有 async void 方法都已完成的时间并收集所有异常,不过只需使 async void 方法改为返回 Task,这会简单得多。
下面的代码演示了这一方法,该方法通过将 async void 方法用于事件处理程序而不牺牲可测试性:
private async void button1_Click(object sender, EventArgs e) { await Button1ClickAsync(); } public async Task Button1ClickAsync() { // Do asynchronous work. await Task.Delay(1000); }
一般而言,仅当 async lambda 转换为返回 Task 的委托类型(例如,Func<Task>)时,才应使用 async lambda。
此例外情况包括逻辑上是事件处理程序的方法,即使它们字面上不是事件处理程序(例如 ICommand.Execute implementations)。
始终使用 Async
此行为是所有类型的异步编程中所固有的,而不仅仅是新 async/await 关键字。
在 MSDN 论坛、Stack Overflow 和电子邮件中回答了许多与异步相关的问题之后,我可以说,迄今为止,这是异步初学者在了解基础知识之后最常提问的问题: “为何我的部分异步代码死锁?”
在调用 Task.Wait 时,导致死锁的实际原因在调用堆栈中上移。
图 3 在异步代码上阻塞时的常见死锁问题
public static class DeadlockDemo { private static async Task DelayAsync() { await Task.Delay(1000); } // This method causes a deadlock when called in a GUI or ASP.NET context. public static void Test() { // Start the delay. var delayTask = DelayAsync(); // Wait for the delay to complete. delayTask.Wait(); } }
它们相互等待对方,从而导致死锁。
当程序员编写测试控制台程序,观察到部分异步代码按预期方式工作,然后将相同代码移动到 GUI 或 ASP.NET 应用程序中会发生死锁,此行为差异可能会令人困惑。
图 4演示了指导原则的这一例外情况: 控制台应用程序的 Main 方法是代码可以在异步方法上阻塞为数不多的几种情况之一。
图 4 Main 方法可以调用 Task.Wait 或 Task.Result
class Program { static void Main() { MainAsync().Wait(); } static async Task MainAsync() { try { // Asynchronous implementation. await Task.Delay(1000); } catch (Exception ex) { // Handle exceptions. } } }
我现在说明错误处理问题,并在本文后面演示如何避免死锁问题。
当没有 AggregateException 时,错误处理要容易处理得多,因此我将“全局”try/catch 置于 MainAsync 中。
请考虑此简单示例:
public static class NotFullyAsynchronousDemo { // This method synchronously blocks a thread. public static async Task TestNotFullyAsync() { await Task.Yield(); Thread.Sleep(5000); } }
图 5 是将同步操作替换为异步替换的速查表。
图 5 执行操作的“异步方式”
| 执行以下操作… | 替换以下方式… | 使用以下方式 |
| 检索后台任务的结果 | Task.Wait 或 Task.Result | await |
| 等待任何任务完成 | Task.WaitAny | await Task.WhenAny |
| 检索多个任务的结果 | Task.WaitAll | await Task.WhenAll |
| 等待一段时间 | Thread.Sleep | await Task.Delay |
此指导原则的例外情况是控制台应用程序的 Main 方法,或是(如果是高级用户)管理部分异步的基本代码。
配置上下文
这可能会形成迟滞,因为会由于“成千上万的剪纸”而降低响应性。
下面的代码段说明了默认上下文行为和 ConfigureAwait 的用法:
async Task MyMethodAsync() { // Code here runs in the original context. await Task.Delay(1000); // Code here runs in the original context. await Task.Delay(1000).ConfigureAwait( continueOnCapturedContext: false); // Code here runs without the original // context (in this case, on the thread pool). }
通过使用 ConfigureAwait,可以实现少量并行性: 某些异步代码可以与 GUI 线程并行运行,而不是不断塞入零碎的工作。
如果需要逐渐将应用程序从同步转换为异步,则此方法会特别有用。
图 6 显示了一个修改后的示例。
图 6 处理在等待之前完成的返回任务
async Task MyMethodAsync() { // Code here runs in the original context. await Task.FromResult(1); // Code here runs in the original context. await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false); // Code here runs in the original context. var random = new Random(); int delay = random.Next(2); // Delay is either 0 or 1 await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false); // Code here might or might not run in the original context. // The same is true when you await any Task // that might complete very quickly. }
图 7 演示 GUI 应用程序中的一个常见模式:让 async 事件处理程序在方法开始时禁用其控制,执行某些 await,然后在处理程序结束时重新启用其控制;因为这一点,事件处理程序不能放弃其上下文。
图 7 让 async 事件处理程序禁用并重新启用其控制
private async void button1_Click(object sender, EventArgs e) { button1.Enabled = false; try { // Can't use ConfigureAwait here ... await Task.Delay(1000); } finally { // Because we need the context here. button1.Enabled = true; } }
图 8 演示的代码对图 7 进行了少量改动。
图 8 每个 async 方法都具有自己的上下文
private async Task HandleClickAsync() { // Can use ConfigureAwait here. await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false); } private async void button1_Click(object sender, EventArgs e) { button1.Enabled = false; try { // Can't use ConfigureAwait here. await HandleClickAsync(); } finally { // We are back on the original context for this method. button1.Enabled = true; } }
即使是编写 ASP.NET 应用程序,如果存在一个可能与桌面应用程序共享的核心库,请考虑在库代码中使用 ConfigureAwait。
此指导原则的例外情况是需要上下文的方法。
了解您的工具
图 9 是常见问题的解决方案的快速参考。
图 9 常见异步问题的解决方案
| 问题 | 解决方案 |
| 创建任务以执行代码 | Task.Run 或 TaskFactory.StartNew(不是 Task 构造函数或 Task.Start) |
| 为操作或事件创建任务包装 | TaskFactory.FromAsync 或 TaskCompletionSource<T> |
| 支持取消 | CancellationTokenSource 和 CancellationToken |
| 报告进度 | IProgress<T> 和 Progress<T> |
| 处理数据流 | TPL 数据流或被动扩展 |
| 同步对共享资源的访问 | SemaphoreSlim |
| 异步初始化资源 | AsyncLazy<T> |
| 异步就绪生产者/使用者结构 | TPL 数据流或 AsyncCollection<T> |
msdn.microsoft.com/library/hh873175),该模式详细说明了任务创建、取消和进度报告。
TPL 数据流和 Rx 都具有异步就绪方法,十分适用于异步代码。
下面是一个异步代码示例,该代码如果执行两次,则可能会破坏共享状态,即使始终在同一个线程上运行也是如此:
int value; Task<int> GetNextValueAsync(int current); async Task UpdateValueAsync() { value = await GetNextValueAsync(value); }
图 10 演示 SemaphoreSlim.WaitAsync。
图 10 SemaphoreSlim 允许异步同步
SemaphoreSlim mutex = new SemaphoreSlim(1); int value; Task<int> GetNextValueAsync(int current); async Task UpdateValueAsync() { await mutex.WaitAsync().ConfigureAwait(false); try { value = await GetNextValueAsync(value); } finally { mutex.Release(); } }
nitoasyncex.codeplex.com) 中提供了更新版本。
而 AsyncEx 提供了 AsyncCollection<T>,这是异步版本的 BlockingCollection<T>。
异步真的是非常棒的语言功能,现在正是开始使用它的好时机!