异步编程最佳实践

简介:

避免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;
  }
}
复制代码

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




本文转自tkbSimplest博客园博客,原文链接:http://www.cnblogs.com/farb/p/4842920.html,如需转载请自行联系原作者


目录
相关文章
|
5月前
|
SQL 数据库 开发者
Python中使用Flask-SQLAlchemy对数据库的增删改查简明示例
这样我们就对Flask-SQLAlchemy进行了一次简明扼要的旅程,阐述了如何定义模型,如何创建表,以及如何进行基本的数据库操作。希望你在阅读后能对Flask-SQLAlchemy有更深入的理解,这将为你在Python世界中从事数据库相关工作提供极大的便利。
510 77
|
5月前
|
人工智能 PyTorch TensorFlow
AI界的"翻译官":ONNX如何让各框架模型和谐共处
还在为不同框架间的模型转换头疼?ONNX让你在PyTorch训练的模型可以无缝在TensorFlow部署,甚至能让模型在手机上飞速运行。本文带你了解这个AI领域的'瑞士军刀',轻松实现跨平台高性能模型部署。
263 12
|
数据采集 自动驾驶 算法
C语言自动驾驶实战项目:基于激光雷达的实时路径规划与避障系统
C语言自动驾驶实战项目:基于激光雷达的实时路径规划与避障系统
|
人工智能 运维 监控
智研未来,直击 AI DevOps,阿里云用户交流日杭州站来啦!
在这个技术日新月异的时代,云上智能化DevOps正以前所未有的速度推动企业创新边界,重塑软件开发的效率与品质。 为深入探索这一变革之路,我们诚邀您参与我们的专属闭门技术沙龙,携手开启一场关于云上智能化DevOps的挑战、实践与未来的展望之旅。
7624 0
智研未来,直击 AI DevOps,阿里云用户交流日杭州站来啦!
|
消息中间件 缓存 前端开发
新项目,不妨采用这种架构分层,很优雅!
新项目,不妨采用这种架构分层,很优雅!
394 0
|
安全 Linux 网络安全
如何使用Nmap进行端口扫描和服务识别?
如何使用Nmap进行端口扫描和服务识别?
1232 0
|
Java API 计算机视觉
常用的视频帧提取工具和方法总结
视频理解任务最基础也是最主要的预处理任务是图像帧的提取。因为在视频理解任务中,视频可以看作是由一系列连续的图像帧组成的。因此,要对视频进行理解和分析,首先需要从视频中提取出每一帧的图像。
899 0
|
存储 弹性计算 运维
Codeup使用评测
Codeup使用评测
776 0
Codeup使用评测
|
存储 NoSQL 算法
SpringBoot 如何进行限流?老鸟们还可以这样玩!
SpringBoot 如何进行限流?老鸟们还可以这样玩!
270 0
|
机器学习/深度学习 人工智能 知识图谱
【文心一言】广告文案、演讲稿与请假条自动生成
作为一名大学生而言,平时参加或者举办一些学校组织的活动的时候,总是避免不了需要准备一些演讲稿、广告宣传文案等内容,甚至于在疫情十分严重的这几年内,如何跟老师“委婉的”请假,也成为了我日常头疼的事情。但在百度推出文心一言以后,我发现这些事情反而变得简单而有趣了起来。
440 0