C#复习笔记(5)--C#5:简化的异步编程(异步编程的基础知识)

时间:2022-05-04 10:21:52

异步编程的基础知识

C#5推出的async和await关键字使异步编程从表面上来说变得简单了许多,我们只需要了解不多的知识就可以编写出有效的异步代码。

在介绍async和await之前,先介绍一些基础的概念:

并发:同时做很多事情。

这个解释直接表明了并发的作用。终端用户程序利用并发功能,在输入数据库的同时响应用户输入。服务器应用利用并发,在处理第一个请求的同时响应第二个请求。只要你希望程序同时做多件事情,你就需要并发。几乎每个软件程序 都会受益于并发。

多线程:并发的一种形式,它采用多个线程来执行程序。

从字面上看,多线程就是使用多个线程。本书后续章节将介绍,多线程是并发的一种形式,但不是唯一的形式。实际上,直接使用底层线程类型在现代程序中基本不起作用。 比起老式的多线程机制,采用高级的抽象机制会让程序功能更加强大、效率更高。因此,本书将尽量不涉及一些过时的技术。书中所有多线程的方法都采用高级类型,而不是Thread或BackgroundWorker。但是,不要认为多线程已经彻底被淘汰了!因为线程池要求多线程继续存在。线程池存放任务的队列,这个队列能够根据需要自行调整。相应地,线程池产生了另一个重要的并发形式:并行处理。

并行处理:把正在执行的大量的任务分割成小块,分配给多个同时运行的线程。

为了让处理器的利用效率最大化,并行处理(或并行编程)采用多线程。当现代多核CPU执行大量任务时,若只用一个核执行所有任务,而其他核保持空闲,这显然是不合理的。并行处理把任务分割成小块并分配给多个线程,让它们在不同的核上独立运行。并行处理是多线程的一种,而多线程是并发的一种。在现代程序中,还有一种非常重要但很多人还不熟悉的并发类型:异步编程。

异步编程:并发的一种形式,它采用future模式或回调(callback)机制,以避免产生不必要的线程。

一个future(或promise)类型代表一些即将完成的操作。在.NET中,新版future类型有Task和Task<TResult>。在老式异步编程API中,采用回调或事件(event),而不是future。异步编程的核心理念是异步操作:启动了的操作将会在一段时间后完成。这个操作正在执行时,不会阻塞原来的线程。启动了这个操作的线程,可以继续执行其他任务。当操作完成时,会通知它的future,或者调用回调函数,以便让程序知道操作已经结束。异步编程是一种功能强大的并发形式,但直至不久前,实现异步编程仍需要特别复杂的代码。VS2012支持async和await,这让异步编程变得几乎和同步(非并发)编程一样容易。

异步编程简介

异步编程的执行流程

现代的异步.NET程序使用两个关键字:async和await。async关键字加在方法声明上,它的主要目的是使方法内的await关键字生效(为了保持向后兼容,同时引入了这两个关键字)。如果async方法有返回值,应返回Task<T>;如果没有返回值,应返回Task。这些task类型相当于future,用来在异步方法结束时通知主程序。还有一种是返回void,这种就是存粹的为了兼容事件处理程序。所以,除了用于注册实践处理程序,不建议在别的地方使用返回void的异步代码。

先来看一个例子:

 async Task AsyncMethod()
{
Console.WriteLine("Sync execute before await");//①同步执行的代码
await Task.Delay(TimeSpan.FromSeconds());//②异步等待,非阻塞
Console.WriteLine("callback method");//③任务的延续
}

和其他方法一样,async方法在开始时以同步方式执行①。在async方法内部,await关键字对它的参数执行一个异步等待②。它首先检查操作是否已经完成,如果完成了,就继续运行(同步方式)。否则,它会暂停async方法,并返回,留下一个未完成的task(token)。一段时间后,操作完成,async方法就恢复运行③。就是这么简单。编译器在后面帮助我们做了大量的工作。在后续的章节中,会详细介绍编译器的所作所为。

同步上下文:一个async方法是由多个同步执行的程序块组成的,每个同步程序块之间由await语句分隔②。第一个同步程序块在调用这个方法的线程中运行,但其他同步程序块在哪里运行呢?情况比较复杂。最常见的情况是,用await语句等待一个任务完成,当该方法在await处暂停时,就可以捕捉上下文(context)。如果当前SynchronizationContext不为空,这个上下文就是当前SynchronizationContext。如果当前SynchronizationContext为空,则这个上下文为当前TaskScheduler。该方法会在这个上下文中继续运行③。一般来说,运行UI线程时采用UI上下文,处理ASP.NET请求时采用ASP.NET请求上下文,其他很多情况下则采用线程池上下文。因此,在上面的代码中,每个同步程序块会试图在原始的上下文中恢复运行。如果在UI线程中调用DoSomethingAsync,这个方法的每个同步程序块都将在此UI线程上运行。但是,如果在线程池线程中调用,每个同步程序块将在线程池线程上运行。要避免这种行为,可以在await中使用ConfigureAwait方法,将参数continueOnCapturedContext设为false。接下来的代码刚开始会在调用的线程里运行,在被await暂停后,则会在线程池线程里继续运行。

可等待模式:关键字await不仅能用于Task,还能用于所有遵循特定模式的awaitable类型——在编译器生成的状态机中,有一个MoveNext的方法,在这个方法中会调用await的对象是否有一个GetAwaiter的方法,这个方法是否会返回一个awaiter,awaiter里面是否包含了GetResult方法和IsCompleted属性,awaiter遵循的接口是INotifyCompletion和ICriticalNotifyCompletion,从名字上面来看就知道他们的意思都是发送一个通知。类似的awaitable类型还有YieldAwaitable、ConfiguredTaskAwaitable,awaitable类型的一个特点是有一个GetAwaiter的方法来返回一个awaiter。

迭代器也是类似的原理,并且它出现的更早(C#2)。在foreach循环一个序列的时候,他不要求这个序列必须实现了IEnumerable或者IEnumerable<T>,foreach会寻找这个序列是否有一个GetEnumerator的方法,这个方法是否返回一个Enumerator,这个Enumerator是否包含一个MoveNext方法和一个返回当前元素的Current属性。

处理异常

因为不确定Task的执行线程,所以Task不会主动的抛出异常,在返回的Task中的Status属性上会指示Task的执行结果,Status属性是TaskStatus枚举类型,定义如下:

 public enum TaskStatus
{
Created,
WaitingForActivation,
WaitingToRun,
Running,
WaitingForChildrenToComplete,
RanToCompletion,
Canceled,
Faulted,
}

Task上面还有一个Exception属性,类型是一个AggregateException。正常情况下,当Task执行顺利完成时,Exception返回null。但Task执行失败时,Exception属性会返回一个AggregateException类型的异常,这个异常包含了Task执行过程中抛出的所有异常。发生异常时,任务结束,不直接抛出异常。只有在使用一个Task的时候,比如Task.Wait()、Task.WhenAll()、等等。还有,在await一个Task的时候,也会抛出异常,但是会抛出AggregateException中的第一个异常。

死锁

关于异步编程还有一个重要的准则就是,在有UI线程的地方,如果你要使用异步编程,就要异步到底,考虑下面的代码:

 async Task WaitAsync()
{
//这里awati会捕获当前上下文……
await Task.Delay(TimeSpan.FromSeconds()); // ……这里会试图用上面捕获的上下文继续执行
}
void Deadlock()
{
//开始 延迟
Task task = WaitAsync(); //同步程序块,正在等待异步方法完成
task.Wait();
}

上面的代码如果在UI线程或Asp.net 中执行,就会发生死锁,来看看到底发生了什么:这两种上下文每次只能运行一个线程。Deadlock方法调用WaitAsync方法,WaitAsync方法开始调用delay语句。然后,Deadlock方法(同步)等待WaitAsync方法完成,同时阻塞了上下文线程。当delay语句结束时,await试图在已捕获的上下文中继续运行WaitAsync方法,但这个步骤无法成功,因为上下文中已经有了一个阻塞的线程,并且这种上下文只允许同时运行一个线程。这里有两个方法可以避免死锁:在WaitAsync中使用ConfigureAwait(false)(导致await忽略该方法的上下文),或者用await语句调用WaitAsync方法(让Deadlock变成一个异步方法)。

基础的东西就这么多,这个章节里面没有多少例子,不过如果能看懂里面的所有意思,那么编写异步编程也不是什么难事。下面的章节会详细的介绍编译器为async和await所做的所有事情,并会将相关的概念进行进一步的扩展。