• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

【转】C#Async/Await异步编程中的最佳做法

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

Async/Await

Stephen Cleary

 

本文只重点介绍一些淹没在文档海洋中的最佳做法。

图 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);
}

返回 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 执行操作的“异步方式”

执行以下操作… 替换以下方式… 使用以下方式
检索后台任务的结果 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.
}

如果方法中在 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 常见异步问题的解决方案

问题 解决方案
创建任务以执行代码 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>。

异步真的是非常棒的语言功能,现在正是开始使用它的好时机!

 

扫码关注微信公众号

https://msdn.microsoft.com/zh-cn/magazine/jj991977.aspx


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
C#获取网站Alaxa排名发布时间:2022-07-14
下一篇:
[C#]遍历对象属性、遍历结构体字段-fqzhang发布时间:2022-07-14
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap