《Illustrated C# 2012, 4th Edition》Daniel M. Solis 第20章 异步编程 笔记

线程

启动程序时,系统会在内存中创建一个进程,进程是运行程序的资源的集合,包含虚地址空间、文件句柄等。
在进程内部,系统会创建一个线程,从Main()方法开始执行。

默认情况下,一个进程只包含一个线程。
一个线程可以派生出其他线程,因此一个进程可以包含多个线程,这些线程共用这个进程的资源。
系统为处理器执行规划的单位是线程,而不是进程。

System.Diagnostics命名空间中的Stopwatch类,可以用来测量一段代码执行的时间。

Stopwatch sw = new Stopwatch();
sw.Start();
// todo...
Console.WriteLine(sw.Elapsed.TotalMilliseconds);

async/await

C# 5.0引入构建异步方法的新特性:async/await,示例:

上面例子的执行流程:

  1. 正常执行,直到在async方法中,遇到await表达式:wb.DownloadStringTaskAsync()。
  2. 因为wb.DownloadStringTaskAsync()会创建一个新的线程,所以async方法中后续代码都在这个新创建的线程中执行。(为了便于区分,将之前的执行线程称为主线程,新创建的线程称为子线程。下同)当子线程创建完毕,返回一个占位符给主线程,这里是Task<int>类型的对象t1和t2。
    • 如果await表达式不会创建一个新的线程,那么会继续正常执行,就像await关键字不存在一样。
  3. 主线程与两个子线程同时执行。
  4. 主线程遇到t1.Result。判断返回t1的async方法,在它对应的子线程中是否执行完毕。如果执行完毕,则t1.Result获取async方法的返回值。如果未执行完毕,则主线程挂起,直到t1对应async方法执行完毕,再从t1.Result获取async方法的返回值。
    • async方法执行完毕时,它对应的子线程自行销毁。
  5. t2.Result的流程与t1.Result相同。

async方法支持三种返回值类型:Task<T>、Task、void。

  1. 返回Task<T>的async方法,需要return一个T类型的值。
  2. 返回Task、void的async方法,不需要return任何值,随使return了某个值也会被忽略。

Thread.Sleep()方法可以挂起当前的线程,参数以毫秒为单位。

await表达式中,await修饰的表达式必须是awaitable类型的对象。
awaitable类型包含GetAwaiter()方法,返回一个awaiter类型对象。
awaiter类型对象包含以下成员:

bool IsCompleted {get;};
void OnCompleted(Action);

awaiter类型对象还包含void GetResult();T GetResult();(T为任意类型)。

Task类(包含泛型版本,下同)是awaitable类型,因此不需要手动创建awaitable类型。
Task.Run()方法返回一个Task类对象。它创建一个子线程,执行作为参数的委托,它的重载如下:

System.Threading.Tasks命名空间中,CancellationToken和CancellationTokenSource类用于取消一个正在执行的async方法,用法如下:

  1. 首先实例化一个CancellationTokenSource对象,通过它的Token属性获得一个CancellationToken对象。
  2. 将这个CancellationToken对象传递给async方法,在方法内部不断手动检测它的IsCancellationRequested属性,如果为false则继续执行,如果为true则取消方法,即return。
  3. 调用CancellationTokenSource对象的Cancel()方法,会将从它获取的CancellationToken对象的IsCancellationRequested属性置为true,从而达到取消async方法的目的。
  • 注意:IsCancellationRequested属性一旦被置为true,就不能再修改,因此CancellationToken对象是一次性的。

在async方法中,当使用try..catch语句捕获到await表达式中的异常时,这个async方法返回的Task类对象的Status属性为RanToCompletion,表示执行完毕,IsFaulted属性为False,表示没有未处理的异常。

阻塞主线程,等待async方法执行完毕:

  1. async方法返回的Task类对象的Wait()方法可以阻塞主线程,直到对应的async方法执行完毕。效果类似于访问它的Result属性,只是不返回一个值。
  2. Task类还提供了两个静态方法WaitAll()和WaitAny(),参数是Task类数组,WaitAll()用于阻塞主线程,直到所有数组元素对应的async方法执行完毕,WaitAny()则用于阻塞主线程,直到任意一个数组元素对应的async方法执行完毕。

WaitAll()和WaitAny()方法还提供以下重载,用于接收取消信号和设置超时的处理:

创建一个子线程,直到目标async方法执行完毕,才开始执行:Task类为此提供了两个静态方法WhenAll()和WhenAny(),参数是Task类数组,WhenAll()用于所有数组元素对应的async方法都执行完毕才开始执行,WaitAny()则用于任意一个数组元素对应的async方法执行完毕就开始执行。

  • WhenAll()和WhenAny()方法必须使用await修饰。

Task.Delay()方法,用于阻塞调用它的async方法所处的线程,参数以毫秒为单位,提供以下重载:

  • Task.Delay()方法必须使用await修饰。

WPF程序从消息泵获取用户输入。因此,如果处理用户输入的时间过长,程序会卡住,用户在程序卡住时的全部操作,会在程序恢复时,一瞬间完成。

Task.Yield()方法,创建一个子线程,将async方法的后续代码(直到下一个会创建线程的await表达式之前)都放到这个线程中执行。

  • Task.Yield()方法必须使用await修饰。

async方法支持Lambda表达式,需要在表达式前添加async关键字。

BackgroundWorker类

BackgroundWorker类的对象用于为一段目标代码创建一个后台线程,当目标代码执行完毕,或提前退出时,销毁这个线程:

BackgroundWorker类的用法:

  1. 创建一个BackgroundWorker类的对象。
  2. 为它的WorkerReportsProgress和WorkSupportsCancellation属性赋值。
    1. WorkerReportsProgress表示用户是否可以调用它的ReportProgress()方法报告目标代码执行进度。
    2. WorkSupportsCancellation表示用户是否可以调用它的CancelAsync()方法,使目标代码提前退出。
  3. 为它的三个事件DoWork、ProgressChanged、RunWorkerCompleted关联方法。
    1. 当用户调用它的RunWorkerAsync()方法时,这个方法会创建一个线程,并触发DoWork事件。通常不会在它的IsBusy属性为true时调用RunWorkerAsync()方法,因为这表示已经存在创建的线程。DoWork事件关联的方法就是目标代码。
    2. 在目标代码中,通常会调用BackgroundWorker类对象的ReportProgress()方法报告进度。ReportProgress()方法会触发它的ProgressChanged事件。
    3. 在目标代码中,还应该不断检查CancellationPending属性,当这个属性为true时及时return。
    4. 当目标代码return后,RunWorkerCompleted事件会触发,在这个事件关联的方法中,通常也会检查CancellationPending属性,以确定目标代码是执行完毕,还是提前退出。在这个事件关联的方法执行完毕后,线程会自行销毁。
  4. 调用它的RunWorkerAsync()方法,开始执行目标代码。
  5. 在目标代码执行过程中,可以随时调用BackgroundWorker类对象的CancelAsync()方法,这个方法会使得它的CancellationPending属性为true,从而使得目标代码提前退出。

并行库(Task Parellel Library)

在System.Threading.Tasks命名空间中提供了Parallel.For和Parallel.ForEach两个并行循环。

Parallel.For有12个重载,最简单的用法如下:

Parallel.For(0, 15, i => Console.WriteLine("The square of {0} is {1}", i, i * i));
  • 第一个参数是起始索引,第二参数比最后一个索引大1,第三个参数是Action委托。
  • 注意:因为是并行执行的,所以不保证迭代顺序。

Parallel.ForEach也有相当多的重载,最简单的用法如下:

string[] squares = new string[] {"We", "hold", "these", "truths", "to", "be", "self-evident", "that", "all", "men", "are", "created", "equal"};
Parallel.ForEach(squares, i => Console.WriteLine(string.Format("{0} has {1} letters", i, i.Length)));
  • 与Parallel.For相同,Parallel.ForEach也不保证迭代顺序。

BeignInvoke和EndInvoke

委托对象的BeignInvoke()方法可以创建一个线程,在这个线程中调用委托的调用列表中的函数。
BeignInvoke()返回一个AsyncResult对象的IAsyncResult接口引用,IAsyncResult接口的IsCompleted属性可以检查委托是否执行完毕。
BeignInvoke()的前几个参数对应调用委托的形参,最后两个参数依次为callback和state。callback为AsyncCallBack委托类型,接收一个IAsyncResult类型参数返回void。callback在调用委托执行完毕时调用。state参数是object类型,会赋值给callback接收到IAsyncResult类型参数的AsyncState属性,用于传递信息。

委托对象的EndInvoke()方法与BeignInvoke()对应,接收一个BeignInvoke()返回IAsyncResult类型参数,返回BeignInvoke()调用委托的返回值,并清理线程。如果调用EndInvoke()方法时,调用未执行完毕,则挂起EndInvoke()方法所处的线程,直到调用委托执行完毕。
EndInvoke()方法还可以返回委托形参列表中的ref和out参数,只要在EndInvoke()的实参列表中,按相同的顺序排列,并使用对应的ref和out,排列在IAsyncResult类型参数之前。

每一个BeignInvoke()方法调用都必须对应一个EndInvoke()方法调用。

异步编程模式

异步编程通常有三种模式:

等待一直到完成(wait-until-done),与async方法相似,原始线程发起异步方法后,进行一些其他处理。然后检查异步方法是否执行完毕,如果未执行完毕,则阻塞原始线程,直到它执行完毕。

轮询(polling),原始线程发起异步方法后,定期检查异步方法是否执行完毕,如果未执行完毕,就去处理其他事情,直到它执行完毕才继续。

回调(callback),原始线程发起异步方法后,就不管异步方法了。直到异步方法执行完毕,在EndInvoke清理线程之前,调用回调方法。

计时器

System.Threading命名空间中的Timer类,它由系统的线程池定期开启线程执行给定的方法。调用的方法必须是TimerCallback委托类型,对应的方法形式为void TimerCallback( object state)
Timer类最常用的构造函数为:

Timer(TimerCallback callback, object state, uint dueTime, uint period)
  • callback为定期调用的方法。
  • state是传递给callback的参数。
  • dueTime为第一次调用时间,设置为0会立刻调用,设置为Timeout.Infinite,则计时器不会工作,即callback一次也不会被调用。
  • period为两次调用之间的时间间隔,如果设置Timeout.Infinite,则callback只调用一次。

.NET BCL还提供了其他的计时器,如System.Windows.Forms.Timer、System.Timer.Timer等。