异步编程最佳实践

避免async void

异步方法返回类型有3种,void,Task和Task<T>,void尽量不要使用。

原理剖析:

使用async void标记的方法有不同的错误处理语义。async Task或async Task<T>方法抛出异常时,异常会被捕获并放到Task对象上。然而,标记为async void的方法没有Task对象,所以async void方法抛出的任何异常都会直接放到SynchronizationContext(异步上下文)上,它是在async void方法开始的时候激活的。下面是一个例子:

//async void 方法不能被捕获的异常
private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception )
  {
    //异常不会被捕获
    throw;
  }
}

async void有不同的组成语法。返回Task或Task<T>的async方法可以使用await Task.WhenAny或Task.WhenAll等轻易组合。而返回void的async方法没有提供简单的方式来通知它们已经完成的调用代码。启用若干个async void方法很容易,但不容易决定它们什么时候完成。async void方法开始和完成时会通知它们的SynchronizationContext,但是自定义的SynchronizationContext对于常规应用代码是一个复杂的解决方案。

async void方法测试很困难。由于错误处理和组合的差异,编写调用async void方法的单元测试很困难。

很明显,async void方法与async Task方法相比有很多劣势,但在一个特殊场合很有用,那就是异步的事件句柄。它们直接将异常抛出到SynchronizationContext,这与同步的事件句柄表现很相似。同步的事件句柄通常是私有的,因此它们不能被组合或者直接测试。我想采取的方法是在异步事件句柄中最小化代码,比如,让它await一个包含实际逻辑的async Task方法,代码如下:

private async void button1_Click(object sender, EventArgs e)
{
  await Button1ClickAsync();
}
public async Task Button1ClickAsync()
{
  //处理异步工作
  await Task.Delay(1000);
}

总之,对于async Task和async void,你应该更喜欢前者。async Task方法更容易错误处理,组合和测试。对于异步的事件句柄异常,必须返回void。

一直使用async

这句话的意思是,不要不经过认真考虑就混合同步和异步代码。特别地,在异步代码上使用Task.Wait或Task.Result是一个馊主意。

下面是一个简单的例子:一个方法阻塞了异步方法的结果。在控制台程序中会工作的很好,但是从GUI或者ASP.Net上下文中调用的时候就会死锁。死锁的实际原因是当调用Task.Wait的时候进一步开启了调用栈。

//阻塞异步代码时的一个常见死锁问题
public static class DeadlockDemo
{
  private static async Task DelayAsync()
  {
    await Task.Delay(1000);
  }
  // 调用 GUI 或 ASP.NET 上下文的时候会造成死锁
  public static void Test()
  {
    // 开始延迟.
    var delayTask = DelayAsync();
    // 等待延迟
    delayTask.Wait();
  }
}

造成这种死锁的根本原因是等待处理上下文的方式。默认情况下,当一个未完成的Task处于被等待状态时,当前上下文会被捕获并且当此任务完成时恢复该方法。这个上下文如果不为null就是当前的SynchronizationContext,在这种情况下,它是当前的TaskScheduler(任务调度者)。GUI 和ASP.NET应用有一个SynchronizationContext,它只允许一次运行一大块代码。当await完成时,它尝试在捕获的上下文内执行异步方法的剩余部分。但是该上下文已经有一个线程了,它在(同步地)等待这个async方法的完成。它们每一个都在等待另一个,造成了死锁。

注意控制台程序不会造成这种死锁。它们有个线程池SynchronizationContext而没有一次执行一大坨代码的SynchronizationContext,因此当await完成时,它在线程池线程上调度该async方法的剩余部分。该方法可以完成,它完成了返回task,并没有死锁。

总之,应该避免混合async和阻塞的代码。这样做的话会造成死锁,更复杂的错误处理和上下文线程不可预测的阻塞。

配置上下文

可以查看我的另一篇博客《Async and Await 异步和等待》的“避免上下文”部分。

这里稍加补充如下:

除了性能方面之外,ConfigureAwait还有另一个重要的方面:它可以避免死锁。在“一直使用async”的代码示例中,再次思考一下:如果你在DelayAsync代码行添加“ConfigureAwait(false)”,那么死锁就会避免。这次,当await完成时,它尝试在线程池上下文内执行async方法的剩余部分。该方法可以完成,完成后返回task,并且没有死锁。这项技术对于逐渐将应用从同步转为异步特别有用。

建议将ConfigureAwait用在方法中的每个await之后。只有当未完成的Task被等待时,才会唤起上下文被捕获;如果Task已经完成了,那么上下文不会被捕获。

async Task MyMethodAsync()
{
  //这里的代码运行在原始 context.
  await Task.FromResult(1);
  //这里的代码运行在原始 context.
  await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false);
  // 这里的代码运行在原始 context.
  var random = new Random();
  int delay = random.Next(2); // delay是 0 or 1
  await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false);
  // 这里的代码不确定是否运行在原始 context.

}

每个异步方法都有自己的上下文,因此如果一个异步方法调用另一个异步方法,那么它们的上下文是独立的。

private async Task HandleClickAsync()
{
  // 这里可以使用ConfigureAwait
  await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
}
private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // 这里不能使用 ConfigureAwait
    await HandleClickAsync();
  }
  finally
  {
    // 返回到这个方法的原始上下文
    button1.Enabled = true;
  }
}

今天就写到这里吧,还有很多很高级的用法,需要自己好好研究一下才能分享出来,希望大家多多支持!



作者:tkb至简 出处:http://www.cnblogs.com/farb/

QQ:782762625欢迎各位多多交流!

本文版权归作者和博客园共有,欢迎转载。未经作者同意下,必须在文章页面明显标出原文链接及作者,否则保留追究法律责任的权利。
如果您认为这篇文章还不错或者有所收获,可以点击右下角的【推荐】按钮,因为你的支持是我继续写作,分享的最大动力!

时间: 2024-10-24 20:00:30

异步编程最佳实践的相关文章

JS编程最佳实践

最近花了一周时间把<编写可维护的js> 阅读了一遍, 现将全书提到的JS编程最佳实践总结如下, 已追来者! 1.return 之后不可直接换行, 否则会导致ASI(自动分号插入机制)会在return 后插入一个分号. 2.一行语句最多不超过80个字符, 如果超过则应该在运算符后换行,并且追加两个缩进. 3.采用驼峰式命名,变量前缀为名词如:myName 函数应该以动词开始如:getName,常量应该以大写字母命名,如:MAX_COUNT, 构造函数首字母大写. 4.数字的写法: 整数:coun

C 编程最佳实践(书写风格)

简介本文是为了满足开发人员的需要而写的.我们总结了一套指南,无论作为开发人员还是顾问,这些指南多年来一直都很好地指导着我们,我们把它们作为建议提供给您,希望对您的工作有所帮助.您也许不赞同其中的某些指南,但我们希望您会喜欢其中的一些并在您的编程或移植项目中使用它们. 风格与指南 * 使用一种使代码具有可读性和一致性的源代码风格.如果没有团队代码风格或自己的风格,您可以使用与大多数 C 程序员采用的 Kernighan 和 Ritchie 风格相似的风格.然而,举一个极端的例子,有可能最终会写出与

一些通过SAP ABAP代码审查得出的ABAP编程最佳实践

1. 这两个IF ELSE分支里检测的条件其实逻辑上来说都是同一类,应该合并到一个IF分支里进行检查: It is an expensive operation to open a file in application server with 50MB file size. Current logic is: 1. Open the file in application server 2. Read the file content line by line 3. If the file i

iOS 8:【转】RESTful编程最佳实践

源地址:http://fann.im/blog/2013/07/15/best-practices-for-restful-api/ 做服务端开发,免不了有对外接口,正好看到 Best Practices for Designing a Pragmatic RESTful API,简单摘抄做个笔记. API 就是面对开发者的 UI,所以要对开发者友好,能方便在浏览器输入访问. 尽量遵守 Web 标准. 使用 RESTful URLs.URL 标识资源,HTTP Method(GET/POST/P

转载 - 读网上文档 - 笔记 - JS 编程最佳实践(Best Practice)

原文地址: 1). JavaScript Best Practices: http://www.w3schools.com/js/js_function_closures.asp 2). JavaScript Closures http://www.w3schools.com/js/js_function_closures.asp 3). JavaScript 严格模式 http://blog.csdn.net/supersky07/article/details/14129179 1. 关于全

Java 编程中关于异常处理的 10 个最佳实践

异常处理是书写 强健 Java应用的一个重要部分.它是关乎每个应用的一个非功能性需求,是为了优雅的处理任何错误状况,比如资源不可访问,非法输入,空输入等等.Java提供了几个异常处理特性,以try,catch和finally 关键字的形式内建于语言自身之中.Java编程语言也允许你创建新的异常,并通过使用  throw 和 throws关键字抛出它们.事实上,异常处理不仅仅是知道语法.书写一个强健的代码更多的是一门艺术而不仅仅是一门科学,这里我们将讨论一些关于异常处理的Java最佳实践.这些 J

多线程和异步编程示例和实践-踩过的坑

上两篇文章,主要介绍了Thread.ThreadPool和TPL 多线程异步编程示例和实践-Thread和ThreadPool 多线程异步编程示例和实践-Task 本文中,分享两则我们在做多线程和异步编程中实际踩过的坑,实际生产环境遇到的问题,以及解决办法. 1. HttpClient 业务场景:使用HttpClient实现第三方业务推送,当第三方的Http服务器不通.或者返回很慢时 线程数暴涨 Asp.Net\Asp.Net MVC场景下,并发多线程导致的线程阻塞:HttpClient.Pos

编程中关于异常处理的10个最佳实践

在实践中,异常处理不单单是知道语法这么简单.编写健壮的代码是更像是一门艺术,在本文中,将讨论java异常处理最佳实践.这些Java最佳实践遵循标准的JDK库,和几个处理错误和异常的开源代码.这还是一个提供java程序员编写健壮代码的便利手册. Java 编程中异常处理的最佳实践 这里是我通过在国内著名的IT培训平台扣丁学堂在线学习收集的10个java编程中进行异常处理的10最佳实践.在Java编程中对于检查异常有褒有贬,强制处理异常是一门语言的功能.在本文中,我们将尽量减少使用检查型异常,同时学

多线程异步编程示例和实践-Thread和ThreadPool

说到多线程异步编程,总会说起Thread.ThreadPool.Task.TPL这一系列的技术.总结整理了一版编程示例和实践,分享给大家. 先从Thread和ThreadPool说起: 1. 创建并启动线程 2. 暂停线程 当前线程在执行Thread.Sleep方法时,会等待指定的时间(1000ms)此时,当前线程处于阻塞状态:WaitSleepJoin 3. 线程等待 当程序运行时,启动了一个耗时较长的线程打印数字,每次打印输出前需要等待1000ms,我们在主程序中调用ThreadJoin方法