多線程之異步操做

專用線程 算法

計算限制的異步操做 編程

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枚舉,從不一樣的異步操做獲取結果則須要從不一樣屬性中獲取。

相關文章
相關標籤/搜索