在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
本文只重点介绍一些淹没在文档海洋中的最佳做法。 图 1 中总结了这些指导原则;我将在以下各节中逐一讨论。 图 1 异步编程指导原则总结
避免 Async Void下面的代码段演示了一个返回 void 的同步方法及其等效的异步方法: void MyMethod() { // Do synchronous work. Thread.Sleep(1000); } async Task MyMethodAsync() { // Do asynchronous work. await Task.Delay(1000); } 返回 void 的 async 方法具有特定用途: 用于支持异步事件处理程序。 但是,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 void 方法是异步的,则这些方法可能会造成严重影响。 一般而言,仅当 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(); } } 这种死锁的根本原因是 await 处理上下文的方式。 它们相互等待对方,从而导致死锁。 当程序员编写测试控制台程序,观察到部分异步代码按预期方式工作,然后将相同代码移动到 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 执行操作的“异步方式”
此指导原则的例外情况是控制台应用程序的 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. } 如果方法中在 await 之后具有需要上下文的代码,则不应使用 ConfigureAwait。 图 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; } } 每个 async 方法都具有自己的上下文,因此如果一个 async 方法调用另一个 async 方法,则其上下文是独立的。 图 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 常见异步问题的解决方案
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>。 异步真的是非常棒的语言功能,现在正是开始使用它的好时机!
扫码关注微信公众号 https://msdn.microsoft.com/zh-cn/magazine/jj991977.aspx |
请发表评论