专用线程 算法
计算限制的异步操做 编程
CLR线程池,管理线程 windows
Task 多线程
协做式取消 异步
Timer async
await与async关键字 异步编程
IO限制的异步操做 函数
Windows的异步IO 学习
APM(APM与Task) 编码
EAP
专用线程
当初学习多线程编程的时候,第一步就是怎么去开一条新的线程,就是new一个Thread的实例,在Jreffy的书中,这种线程称做为专用线程。它与线程池中的线程区别开来。虽然然先后二者都是Thread类的对象,可是专用线程在使用了一次以后就没法再使用了;而线程池的线程则是能够屡次被使用,有任务执行时就唤醒,空闲的时候就休眠,长期休眠则自我结束。无论是专用线程仍是线程池中的线程,都是CLR线程,CLR线程是一种逻辑线程,在目前Windows平台来讲CLR线程对应着windows线程。无论是怎么样的线程,在异步操做中都发挥着不可或缺的做用。
线程池
下面则来介绍线程池,线程池的线程分两类,一类是用于处理计算限制操做;另外一类是处理IO限制操做。通常咱们能直接调用线程池的线程来完成操做时所用到的线程是计算限制操做的线程。将一个操做交给线程池的线程去执行与往常的new Thread()不一样,从外部来看是得不到任何一个Thread实例,而是把这个操做包装成一个工做项(WorkItem)做为一个请求递交给线程池(也就是调用ThreadPool类的QueueUserWorkItem方法),有请求则会有响应。响应的时间不肯定:如果线程池有空闲线程,能够立刻执行该工做项;如果线程池没有空闲线程而线程数量未达到上限时,线程池会new一个Thread来执行这个工做项;如果没有空闲线程且线程数量已经达到上限,那往线程池递交请求的线程则会被阻塞,直到线程池腾出空闲的线程接收了该工做项,被阻塞的线程才会恢复,工做项才会被处理。
在线程池内部,盗用了《CLR via C#》书中的一幅图,在线程池中每一个计算限制操做的线程(就是工做者线程)都拥有一个队列存放工做项,而另外还有一个全局队列。
往线程池递交的工做项先放到全局队列中。而后空闲的工做者线程会往全局队列中竞争获取一个工做项,获取工做项会采用先进先出算法。竞争回来的工做项会放到本地队列中,工做者线程就会在本地队列中经过先进后出的算法获取一个工做项来处理,假设本地队列中没有工做项可处理,它会到别的本地队列中经过先进先出算法竞争获取一个工做项来处理,固然后者出现的概率都不多。当全部队列都是空的状况下,空闲的工做者线程就会开始休眠。
Task
Task类是在.NET Framework4中引入的,其实际是对线程池调用的封装,可是Task倒是这个异步操做变得更简单直观并更好操做。在Task范畴内有三个比较关键的对象,一个是Task自己,表明着任务的实体;另外一个是TaskFactory,用于构建一个Task;还有一个是TaskScheduler,用于调度Task,控制Task的执行时机。下面则逐一介绍。
开启一个Task的方式有如下几种
new Task(action, "alpha").Start(); Task.Run(()=>action("alpha")); Task.Factory.StartNew(action, "beta");
其中最后一个是用了TaskFactory实现的。
Task能够与Thread同样经过阻塞当前线程以等待异步操做的完成,只需经过调用Wait()方法。固然这个等待完成的方式可有多种,Wait只是单纯等待一个完成,除此以外还有等待全部Task完成的WaitAll和等待一堆Task的某一个完成的WaitAny。Task能够获取执行的结果,这与Thread和ThreadPool有所区别,经过泛型Task<TResult>的Result属性能够获取异步操做中返回的对象,固然获取结果天然要等待执行完毕,必先调用Wait,所以调用线程则会受到阻塞,
Task<Int32> t=new Task<Int32>(n=>Sum((Int32),),100000); t.Start(); t.Wait(); Console.WriteLine("The Sum is :"+t.Result);
假设Task执行过程当中出现了异常,该异常也会在调用Result属性或者Wait的时候被抛出。之因此调用这两个都会去抛出是由于它们不必定同时调用,有时候只调用Wait;有时候只调用Result。就好比下面要介绍的ContinueWith方法。把上面的方法稍微改动一下
Task<Int32> t=new Task<Int32>(n=>Sum((Int32),),100000); t.Start(); t.ContinueWith(t=>Console.WriteLine("The Sum is :"+t.Result));
这样就毋需调用Wait来阻塞当前线程等待Task执行完毕得出结果,ContinueWith传进去的其实是一个回调,顾名思义是等Task执行完毕以后再执行回调,固然回调通常状况下也是经过线程池的线程来执行。因为ContinueWith返回的是一个Task,故能够按照须要后面调用一个或多个ContinueWith。这就是JQuery里面提到的链式操做,这种写法也可让代码更优雅,免去传统写法中在多个回调层层嵌套的状况,其实微软的这个设计却是不错的,能够借鉴到自定义的一些回调操做中。ContinueWith能够的一个重载带TaskContinuationOpetions枚举的参数,指定了这个参数来讲明这个回调是在必定条件下才调用。例如NotOnFaulted则是在非失败的时候执行,NotOnCanceled则是在非取消的时候执行。还有其余的能够在MSDN上获取。
Task除了提供ContinueWith这种机制外,还提供了父子任务这一机制,凡是在一个Task里面再建立的子Task,父级Task自动地等待全部子级Task执行完毕后才完成。毋需显式地调用Wait。
Task<Int32 []> parent=new Task<Int32[]>(()=>{ Var result=new Int32[3]; New Task(()=>{result[0]=Sum(1000)},TaskCreationOperations.AttanchedToParent).Start(); New Task(()=>{result[1]=Sum(2000)},TaskCreationOperations.AttanchedToParent).Start(); New Task(()=>{result[2]=Sum(3000)},TaskCreationOperations.AttanchedToParent).Start(); }); Parent.ContinueWith(t=>Array.ForEach(t.Result,Console.WriteLine)); Parent.Start();
这里又再次抄袭了《CLR via C#》的代码。展现这个代码只是为了说明父子Task的关系创建在TaskCreationOperations枚举的AttanchedToParent值上。
TaskFactory顾名思义就是构建Task的工厂,它存在的意义是便于建立多个设置相同的Task对象,这些设置包括TaskCreationOpeartions,TaskContinuationOperations,CancellationToken和TaskScheduler。同时还有一个便利的地方是统一对工厂建立的各个Task使用Continue方法。可是比价糟糕的是TaskContinuationOperations的那几个NotOn和OnlyOn的值都是非法的。若须要使用这几个值的Continue仍是要经过遍历全部Task逐一去调用。
TaskScheduler是负责定义Task调度的逻辑,肯定让其啥时候执行,如何执行。在FCL中默认定义了两个(但我在4.6的源码中看到的是三个)任务调度器:ThreadPoolTaskScheduler(线程池任务调度器)和SynchorizationContextTaskScheduler(同步上下文任务调度器),还有一个是ConcurrentExclusiveTaskScheduler。线程池任务调度器则是默认Task的任务调度器,默认的Task之因此是用线程池的线程就是由于使用了这个调度器,同时也是经过这个调度器实现了Task在线程池的各个队列中存储以及被执行这些逻辑。而同步上下文任务调度器则是给WinForm和WCF等应用程序中的UI线程调用的。由此看来,Task这个体系的职责切分的比较细,Task包含了任务的内容,Factory负责构造,执行由Schedule来处理,这样万一在那方面不符合需求均可以进行扩展,整个体系的结构不须要修改。为了引证TaskSchedule的做用还作了一个小小的实验,先看如下代码
Action让线程休眠了10秒而后输出一条信息。这个操做放在一个专用线程上执行,默认的专用线程是非后台线程,须要它执行完毕,程序才能够执行完毕。
那下面则把专用线程换成Task执行,
程序并无等待信息输出就执行完毕了,这是因为默认的调度器是线程池调度器,线程池的线程是后台线程,只要主线程结束了,后台线程不管是否执行完毕都会被结束掉,所以没法看到信息输出。那就意味着我指定一个调度器是用专用线程来执行,就可让其正常休眠10秒后输出消息,最后结束运行。为此我定义了一个TaskScheduler,
定义一个TaskFacotry使用上这个ThreadTaskScheduler
执行了一下果真能让这个Task在专用线程上执行,在等待了10秒后输出了信息。按照这样的方式我也定义了一个相似于定时调用的调度器,可是这里走了点野路子,结果仍是凑效
额外提一下是继承TaskScheduler这个抽象类须要重写三个方法
在看了FCL的源码才发现,实际上执行Task会在后面两个方法中执行,若是QueueTask中没有执行的话,会在TryExecuteTaskInline中再执行一次。
协做式取消
这个内容并不是是以某种方式执行一个异步操做,但涉及到一个异步操做的执行过程,故说起之。在往常想结束一条正在执行的线程的方式有两种,一种是经过Thread的Abort方法,这种方式有两个弊端,结束不可控,没法确保线程真的是结束了或者是在哪里结束;只有获取到Thread这个对象才能够调用。那另外一种方式是在关键位置设一个标识变量,该变量就用于表示当前操做是否应该要结束了。在外部若是须要结束则改变这个变量的值就行,这种方式就比较野,并且关系到线程同步问题每每每次都须要本身去处理。不过这也是协做式取消了,在FCL中提供了CancellationTokenSource类来实现这种模式。用法比较简单,在须要取消操做的地方调用方法Cancel()方法则可,那么如何在执行过程当中判断是否已被取消呢?CancellationTokenSource的Token属性返回一个CancellationToken类型的结构体,像Task中所用到的都是这个CancellationToken的结构体而已,调用这个结构体的IsCancellationRequested属性就能够得知该操做是否有被取消了。多个地方须要由同一个对象控制它是否须要结束,则从同一个CancellationTokenSource中获取Token则可。因为慵懒则又抄袭例程
Timer
要让操做按期执行或者按必定周期执行这个应用场景确定不会陌生,野路子就是new 一个Thread,而后里面就执行一个死循环,预先计算这个周期是多长时间,而后在死循环里不断地执行操做和Sleep。正宗的路子使用Timer,FCL中有很多的Timer,但Jeffrey最推荐的就是System.Threading.Timer。这个类的其中一个构造函数以下
public Timer(TimerCallback callback, object state, int dueTime, int period);
callback则是被按期执行的操做,dueTime则是首次执行的延迟时间,指定 System.Threading.Timeout.Infinite 可防止启动计时器。指定零(0) 可当即启动计时器。调用 callback 的时间间隔(以毫秒为单位)。指定 System.Threading.Timeout.Infinite或者-1 能够禁用按期终止,也就是说只会调用一次。Timer一旦被构造,就会立刻开始运行(并不是意味着执行callback,由于还有dueTime)
同时若是须要更改执行周期,可使用Change方法
public bool Change(int dueTime, int period);
那就是说想让一个操做指定在某个时间执行,或者是重复执行均可以用这个Timer。
async关键字和await关键字
用关键字async声明的方法说明它里面包含了异步调用,其返回值是void,Task或Task<Tresult>。MSDN上说,这是一个异步方法。
await关键字则使用在带有aysnc声明的方法中,使用了这个这个关键字的方法必定是返回Task或Task<Tresult>的,而不能是void的。(能够是void的)。这就说明了假设要用await而实际的方法中不须要返回一些操做(或运算)后得出的结果的,则返回Task(或void);不然须要返回结果的,则用Task<Tresult>。带了await关键字的语句后面的语句将会与await前面的语句不在同一条线程中执行。即在async方法中,执行await先后的线程是不同的,不然不带await的话整个方法都由同一个线程执行,且该线程是调用本方法的线程。
经过这段代码引证上述观点,
在TestMain,PrintThreadId和GetValueAsync中分别打印出线程Id,GetValueAsync方法中用了Task,确定跟TestMain的线程Id不同,而在PrintThreadId中,因为没有使用await关键字,所以调用线程GetValueAsync后立刻执行下面的另外一个WriteLine方法,运行结果以下
而把上述代码稍做修改
这样线程调用完await时就会立刻从PrintThreadId方法中返回,Main Method 与 Async Method 1两个地方输出的线程Id是一致的,跟Async Method 2输出的线程Id是不一致的。
public async void DisplayValue() { double result = await GetValueAsync(1234.5, 1.01);//此处会开新线程处理GetValueAsync任务,而后方法立刻返回 //这以后的全部代码都会被封装成委托,在GetValueAsync任务完成时调用 System.Diagnostics.Debug.WriteLine("Value is : " + result); }
上面这段代码等价于下面这段代码,System.Diagnostics.Debug.WriteLine("Value is : " + result);被放到一个委托中,待GetValueAsync里面的异步代码执行完毕以后才调用该委托。
public void DisplayValue() { System.Runtime.CompilerServices.TaskAwaiter<double> awaiter = GetValueAsync(1234.5, 1.01).GetAwaiter(); awaiter.OnCompleted(() => { double result = awaiter.GetResult(); System.Diagnostics.Debug.WriteLine("Value is : " + result); }); }
IO限制操做
Windows执行IO操做
下面经过两幅图说明如何在Windows中执行同步和异步的IO操做
如上图就是Windows执行一个同步IO的过程,首先调用FileStream的Read方法,内部就会调用Win32的ReadFile函数,接着就会把这个操做封装成一个IO请求包(IO Request Package,简称IRP),接着会调用内核的方法,内核方法会把IRP放到对应的IO设备的一个IRP队列中,这个队列是各个IO设备都有一个并独立维护,IPR放到队列中就等待着被设备处理,此时线程就会被阻塞(实际上这里是阻塞仍是休眠还不知道,由于书中文字上写的是休眠,可是图片中写的是阻塞),等处处理完成才会逐级往上返回。
异步的IO跟同步IO的前4步都大体同样,有细微区别在于第一步调用的是ReadAsync。在IRP放到IP队列中时,线程就能够立刻返回,去干别的事情,免去了傻等。当设备处理到这个IRP时,IRP里面记录着一个IO完成的回调,此时就能够往线程池发出一个请求去执行这个回调,这里调用的应该是线程池的IO线程吧。
APM
APM其实是Asynchoronous Programming Model(异步编程模型)的简称,在日常编码中会发现有些方法是以BeginXXX和EndXXXX前缀,这就是传说中的APM了。与同步的区别是调用了BeginXXX方法后它会立刻返回并不会阻塞当前线程,而后调用完毕后它须要调用EndXXX方法获取调用的结果,这个方法最好是放在回调方法中执行,由于若是异步调用还没完成的话EndXXX会对调用线程进行阻塞,而EndXXX同时也是必定要去调用的,不然异步操做会占用掉线程池的一条线程,不结束调用该线程会被白白地占用着,浪费了资源。支持APM的类有System.IO.Stream及其派生类,System.Net.Sockets.Socket,System.Net.WebRequest及其派生类等。可是特别地System.IO.Stream里面的APM操做并不是真正地执行异步IO操做。另外,像Action也提供了APM,可是它执行的计算限制操做,也并不是是异步IO操做。
如上面例程所示,全部BeginXXX方法除了须要传入原有方法的一些参数外,还须要传入AsyncCallback的回调以及一个object类型的state参数,同时返回一个IAsyncResult类型的对象,不过该对象通常不须要理会,它会在AsyncCallback方法中传进去,在IAsyncResult对象中有一个AsyncState属性,获取的就是传进去的State对象,调用EndXXX方法时也须要吧这个IAsyncResult对象传进去,若是本来方法有结果返回的,则从EndXXX中获取返回的结果。万一在异步操做过程当中发生了异常,异常会在调用EndXXX方法时会抛出,假设并无调用EndXXX方法,这个异常会直接让CLR崩掉,致使程序直接退出。
因为Task实现了IAsyncResult接口,所以它对APM也提供必定的支持,下面代码段展现如何利用Task实现APM
EAP
EAP是Event-based Asynchronous Pattern(基于事件的异步模式)的简称,关于这种模式的优劣众说纷纭,微软官网上有一篇文章挺赞这种模式,而Jeffrey则批这种模式,至少在Socket编程时,EAP会优越于APM。
EAP模式是经过XXXAsync方法开始了异步调用,而异步调用完成后就会触发XXXCompleted事件
EAP一样在Task中有支持
EAP的异常不会抛出,要查看异步调用是否发生了异常须要在AsyncCompletedEventArgs的Exception属性中看它是否为null,要判断异常类型则须要用if和typeof,并不是是catch块,若是不去管异常,程序也可继续运行。记得当时对比过APM和EAP,说APM每次都要生成一个IAsyncResult对象,会耗费内存,而EAP的EventArgs能够重重复利用。
实际上Jeffrey在书中举的例子较为恰当,由于他用的是WebClient这个类,Complete事件是在WebClient里面的,而Socket的Complete是在Arg事件参数里面的,Socket感受是实现EAP的一个特例,难怪Jeffrey列举的支持EAP的类没有它了,其余支持EAP的类的事件参数都继承AsyncCompletedEventArgs,
均可以经过Error属性来查看异步调用是否有异常发生过,或者Cancelled查看是否被取消,而因为各个具体的异步调用所获取的结果不同,结果就在他们的继承类才定义,例如
对比Socket,它全部异步操做都是用同一个事件参数
发生异常查询的并不是是异常类,而是一个SocketError枚举,从不一样的异步操做获取结果则须要从不一样属性中获取。