C#系列之.net异步编程的前世今生 & async & await

.net异步编程的前世今生

.NET Framework 提供了执行异步操作的三种模式:

  • 异步编程模型 (APM,Asynchronous Programming Model) 模式(也称 IAsyncResult 模式),在此模式中异步操作需要 Begin 和 End 方法(比如用于异步写入操作的 BeginWrite 和 EndWrite)。 对于新的开发工作不再建议采用此模式
  • 基于事件的异步模式 (EAP,Event-based Asynchronous Pattern),这种模式需要 Async 后缀,也需要一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。 EAP 是在 .NET Framework 2.0 中引入的。 对于新的开发工作不再建议采用此模式。
  • 基于任务的异步模式 (TAP, Task-based Asynchronous Pattern) 使用一种方法来表示异步操作的启动和完成。 TAP 是在 .NET Framework 4 中引入的,并且它是在 .NET Framework 中进行异步编程的推荐使用方法。 C# 中的 async 和 await 关键词以及 Visual Basic 语言中的 Async 和 Await 运算符为 TAP 添加了语言支持。

现有一个从指定偏移量处起将指定量数据读取到提供的缓冲区中的Read方法:

APM:

EAP:

TAP:

一个例子

同步版

EAP版

一个简单的下载逻辑被分隔到了两个方法中,在第一个方法中挂载 DownloadFileCompleted 事件,然后启动下载。下载完成后通过 DownloadFileCompleted 事件处理函数进行通知。

TAP版:

前台线程和后台线程

.Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。

.Net环境使用Thread建立的线程默认情况下是前台线程,即线程属性IsBackground=false,在进程中,只要有一个前台线程未退出,进程就不会终止。主线程就是一个前台线程。而Task.Run使用的是后台线程。后台线程不管线程是否结束,只要所有的前台线程都退出(包括正常退出和异常退出)后,进程就会自动终止。一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。

需要明白的概念性问题:

1. 线程是运行在进程上的,进程都结束了,线程也就不复存在了!

2. 只要有一个前台线程未退出,进程就不会终止!即说的就是程序不会关闭!(即在资源管理器中可以看到进程未结束。)

核心:Task

Task和Thread的区别:
1.Task是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
2.Task底层是使用线程池的,而Thread每次实例化都会创建一个新的线程

Task的状态

成员名称 说明
Canceled

该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的 CancellationToken 发出了信号。 有关详细信息,请参阅任务取消

Created

该任务已初始化,但尚未被计划。

Faulted

由于未处理异常的原因而完成的任务。

RanToCompletion

已成功完成执行的任务。

Running

该任务正在运行,但尚未完成。

WaitingForActivation

该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。

WaitingForChildrenToComplete

该任务已完成执行,正在隐式等待附加的子任务完成。

WaitingToRun

该任务已被计划执行,但尚未开始执行。

Task的生命周期

async和await

定义一个异步方法

  • 方法的声明中有async修饰符。
  • 按照约定,方法名字里有Async后缀。
  • 返回值类型是下面三种情况:
  • * Task,当方法里没有return语句或者return语句没有操作数。
    * Task\,方法里有return语句,并且返回值类型是TResult。
    * void,编写的是一个异步的event handler。

  • 方法通常包含至少一个await表达式。
  • await针对的是Task对象
  • main方法不能加async关键字,因此在main方法中也不能用await,但是可以调用Task.Wait()或Task.Result()获得结果

async await的执行顺序:

执行结果

从执行结果可以看出,
1. 主线程会一直顺序执行
2. 碰到第一个await会返回main方法,继续执行,
3. 由于碰到了task.Result,必须要等待异步执行结果
4. Task.Run启动了一个新线程,而GetLengthAsync方法中await之前的部分运行在主线程上,而await之后的代码运行在一个新的线程上,可以理解为await关键字把await之后的部分编译成了一个类似回调函数的方法,因此运行在与原来代码不一样的线程上。

思考时间:下面两种写法,它们有什么区别?
现在有两个异步方法

实现1.

实现2.

结论:
实现1是同时启动了Task1和Task2,而实现2其实是一个顺序执行,因为await关键字,先执行了Task1,待完成后才执行Task2

I/O Bound and CPU Bound

1. 你的代码是否会“等待”某些内容,例如数据库中的数据?

如果答案为“是”,则你的工作是 I/O Bound。

2. 你的代码是否要执行开销巨大的计算?

如果答案为“是”,则你的工作是 CPU Bound。

如果你的工作为 I/O 绑定,请使用 asyncawait(而不使用 Task.Run)。 不应使用任务并行库。 相关原因在深入了解异步的文章中说明。

如果你的工作为 CPU 绑定,并且你重视响应能力,请使用 asyncawait,并在另一个线程上使用 Task.Run 生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。

我要等我的Task小伙伴(WhenAll & WhenAny)

WhenAll
创建一个任务,该任务将在数组中的所有 Task 对象都完成时完成。

WhenAny
任何提供的任务已完成时,创建将完成的任务。

和Task小伙伴接力跑(ContinueWith)

我不想要我的Task小伙伴了(CancellationToken)

在自己的实现逻辑里用

在外部用tokenSource.Cancel();控制

References

Task类

可恶的I/O-Bound工作

异步编程

Asynchronous Programming Patterns

打赏

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.