C#的变迁史04 - C# 4.0 之多线程篇
在.NET 4.0中,并行計算與多線程得到了一定程度的加強,這主要體現(xiàn)在并行對象Parallel,多線程Task,與PLinq。這里對這些相關(guān)的特性一起總結(jié)一下。
使用Thread方式的線程無疑是比較麻煩的,于是在這個版本中有了改善的版本Task。除了運行效率等方面的提升,Task還與并行計算緊緊聯(lián)系在了一起,這些線程充分的利用了多核的優(yōu)勢,在一定的場合下,大幅的提高了程序的運行效率。屏蔽了運行細(xì)節(jié)的Task和Parallel方式使得程序員們完全不用編寫任何針對多核的程序,只需要使用標(biāo)準(zhǔn)的類庫完成任務(wù)就可以了,其它的CLR會去處理。
這一篇中先看第一個利器:多線程Task。
Task類為把線程類進(jìn)行改良,使之使用起來更簡便,更加容易。要想開啟一個新的線程執(zhí)行任務(wù),只要調(diào)用Task.Factory.StartNew方法就可以了,執(zhí)行完這個語句后線程就開始運行了。當(dāng)然了,使用new初始化一個Task,然后適時調(diào)用其Start方法開始運行也是很不錯的一個選擇。
using System; using System.Threading.Tasks;class Program {static void Main(string[] args){ var task = Task.Factory.StartNew(() =>{for (int i = 0; i < 100; i++) { Console.Write('B'); }});task.ContinueWith(t =>{Console.WriteLine();Console.WriteLine("sub task {0} done", t.Id);});for (int i = 0; i < 100; i++) { Console.Write('A'); }task.Wait();} }注意最后的task.Wait(),調(diào)用這個方法是等待子線程執(zhí)行結(jié)束,當(dāng)需要等待子線程結(jié)果的時候,它最有用。
task對象還有很多有用的方法,從它們的名字就可以知道它們各自的用途了,其中實例方法如上面的ContinueWith方法,它會在task執(zhí)行完畢后執(zhí)行其參數(shù)指定的行為;靜態(tài)方法如WaitAny,WaitAll等,它們指定了在執(zhí)行多個task時主線程的等待條件。
對于多線程編程來說,啟動線程并等待其自然結(jié)束是最常見的一種應(yīng)用,處理也比較簡單。相比而言,線程的中途終止和異常的處理要麻煩的多,難以預(yù)計的隱藏bug會出現(xiàn)在線程程序中。
線程的終止類型
線程執(zhí)行的任務(wù)結(jié)束以后,線程就正常結(jié)束了。這里線程任務(wù)結(jié)束通常有3種情況:任務(wù)正常執(zhí)行完,任務(wù)被取消,任務(wù)被異常打斷結(jié)束。查詢Task結(jié)束時的狀態(tài)類型就是調(diào)用相關(guān)的屬性,如下面的例子:
static void Main(string[] args) {Task t = new Task(() =>{Console.WriteLine("任務(wù)開始工作……");//模擬工作過程Thread.Sleep(5000);});t.Start();t.ContinueWith((task) =>{Console.WriteLine("任務(wù)完成,完成時候的狀態(tài)為:");Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);});Console.ReadKey(); }Task對象的三個屬性IsCompleted,IsCanceled,IsFaulted就是查詢線程的執(zhí)行情況,不過需要注意,只要線程結(jié)束了,不管是以什么方式,IsCompleted始終返回true。IsCanceled代表程序被主動取消了,IsFaulted代表線程出現(xiàn)異常被動結(jié)束,這兩個值都為false,代表線程是正常執(zhí)行完結(jié)束的。
正常結(jié)束的情況比較簡單,這里就不多說了,這里看一下子線程任務(wù)的取消問題。這在編程中是很常見的一個需求,啟動了一個線程以后,發(fā)現(xiàn)某些條件具備了,就不需要線程繼續(xù)運行了,這個時候就需要取消線程任務(wù)。
主動取消/中止線程的標(biāo)準(zhǔn)做法
在以前的版本中,我基本上是都通Thread的Abort方法強行的中止線程。在C# 4.0中,標(biāo)準(zhǔn)的取消一個線程任務(wù)的做法是使用協(xié)作式取消(Cooperative Cancellation)。協(xié)作式取消的機制是,如果線程需要被停止,那么線程自身就得負(fù)責(zé)開放給調(diào)用者這樣的接口:Cancled,然后線程在工作的同時,不斷以某種頻率檢測Cancled標(biāo)識(通常是把任務(wù)主體包裝到循環(huán)中),若檢測到Cancled,線程自己負(fù)責(zé)退出。
下面是一個最基礎(chǔ)的協(xié)作式取消的樣例:
// 設(shè)定取消標(biāo)識 CancellationTokenSource cts = new CancellationTokenSource(); Thread t = new Thread(() =>{while (true){// 檢查取消標(biāo)識if (cts.Token.IsCancellationRequested){Console.WriteLine("線程被終止!");break;}Console.WriteLine(DateTime.Now.ToString());Thread.Sleep(1000);}});t.Start(); Console.ReadLine(); // 主線程申請取消 cts.Cancel();調(diào)用者使用CancellationTokenSource的Cancle方法通知工作線程退出。工作線程則以一定的的頻率一邊工作,一邊檢查是否有外界傳入進(jìn)來的Cancel信號。若有這樣的信號,則負(fù)責(zé)退出。可以看到,在正確停止線程的機制中,真正起到主要作用的是線程本身,它負(fù)責(zé)檢測相關(guān)信號并退出。
協(xié)作式取消中的關(guān)鍵類型是CancellationTokenSource。它有一個關(guān)鍵屬性Token,Token是一個名為CancellationToken的值類型。CancellationToken繼而進(jìn)一步提供了布爾值的屬性IsCancellationRequested作為需要取消工作的標(biāo)識。CancellationToken還有一個方法尤其值得注意,那就是Register方法。它負(fù)責(zé)傳遞一個Action委托,在線程停止的時候被回調(diào),使用方法如:
cts.Token.Register(() => {Console.WriteLine("工作線程被終止了。"); });而且Task對象對CancellationTokenSource對象是天生支持的,在構(gòu)造Task對象的時候就可以傳進(jìn)去CancellationTokenSource的實例。看一個網(wǎng)上的小例子:
static void Main(string[] args) {CancellationTokenSource cts = new CancellationTokenSource();Task<int> t = new Task<int>(() => Add(cts.Token), cts.Token);t.Start();t.ContinueWith(TaskEnded);//等待按下任意一個鍵取消任務(wù)Console.ReadKey();cts.Cancel();Console.ReadKey(); }static void TaskEnded(Task<int> task) {Console.WriteLine("任務(wù)完成,完成時候的狀態(tài)為:");Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);Console.WriteLine("任務(wù)的返回值為:{0}", task.Result); }static int Add(CancellationToken ct) {Console.WriteLine("任務(wù)開始……");int result = 0;while (!ct.IsCancellationRequested){result++;Thread.Sleep(1000);}return result; }不過需要注意,像上面這么寫,使用ct.IsCancellationRequested判斷一下,如果取消信號被設(shè)定了,則退出任務(wù),這種情況CLR會認(rèn)為是成功結(jié)束的,這切實反應(yīng)了程序員的期望,IsCanceled會返回false。如果想出現(xiàn)IsCanceled為true的情況,那么程序就要改寫成:
static void Main(string[] args) {CancellationTokenSource cts = new CancellationTokenSource();Task<int> t = new Task<int>(() => AddCancleByThrow(cts.Token), cts.Token);t.Start();t.ContinueWith(TaskEndedByCatch);//等待按下任意一個鍵取消任務(wù)Console.ReadKey();cts.Cancel();Console.ReadKey(); }static void TaskEndedByCatch(Task<int> task) {Console.WriteLine("任務(wù)完成,完成時候的狀態(tài)為:");Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);try{Console.WriteLine("任務(wù)的返回值為:{0}", task.Result);}catch (AggregateException e){e.Handle((err) => err is OperationCanceledException);} }static int AddCancleByThrow(CancellationToken ct) {Console.WriteLine("任務(wù)開始……");int result = 0;while (true){ct.ThrowIfCancellationRequested();result++;Thread.Sleep(1000);}return result; }在任務(wù)結(jié)束求值的方法TaskEndedByCatch中,如果任務(wù)是通過ThrowIfCancellationRequested方法結(jié)束的,對任務(wù)求結(jié)果值將會拋出異常OperationCanceledException,而不是得到拋出異常前的結(jié)果值。這意味著任務(wù)是通過異常的方式被取消掉的,所以可以注意到上面代碼的輸出中,狀態(tài)IsCancled為true。同時你會發(fā)現(xiàn)IsFaulted狀態(tài)卻還是等于false。這是因為ThrowIfCancellationRequested是協(xié)作式取消方式類型CancellationTokenSource的一個方法,CLR進(jìn)行了特殊的處理。CLR知道這一行程序開發(fā)者有意為之的代碼,所以不把它看作是一個異常(它被理解為取消)。要得到IsFaulted等于true的狀態(tài),自己手動在一個地方拋出一個異常試試就可以了。
此外,CancellationTokenSource就是可以被多個Task共享的,這樣可以取消一組任務(wù)。取消一組任務(wù)最簡單的就是使用任務(wù)工廠。任務(wù)工廠支持多個任務(wù)之間共享相同的狀態(tài),如取消類型。通過使用任務(wù)工廠,可以同時取消一組任務(wù):
static void Main(string[] args) {CancellationTokenSource cts = new CancellationTokenSource();//等待按下任意一個鍵取消任務(wù)TaskFactory taskFactory = new TaskFactory();Task[] tasks = new Task[]{taskFactory.StartNew(() => Add(cts.Token)),taskFactory.StartNew(() => Add(cts.Token)),taskFactory.StartNew(() => Add(cts.Token))};//CancellationToken.None指示TasksEnded不能被取消taskFactory.ContinueWhenAll(tasks, TasksEnded, CancellationToken.None);Console.ReadKey();cts.Cancel();Console.ReadKey(); }static void TasksEnded(Task[] tasks) {Console.WriteLine("所有任務(wù)已完成!"); }好了,看完線程的正常取消,再來看一下線程的異常問題,這個在上面也簡單說了一下,這是一種程序員不期望的線程結(jié)束的方式。
線程的異常處理
先看下面的例子:
Task.Factory.StartNew(() => {throw new Exception(); });? 運行這段程序,你會發(fā)現(xiàn)根本沒有異常拋出,線程中的異常會被線程忽略掉,這個是我們不需要的,我們需要知道異常發(fā)生了,并進(jìn)行相應(yīng)的處理。
跟蹤這種問題,通常記日志是一種常用方法。此外通過技術(shù)手段去捕獲這些異常時另一種方式,這是這里討論的重點。
Task線程中未捕獲的異常會在垃圾回收時終結(jié)器執(zhí)行線程中被拋出。我們可以通過GC.Collect來強制垃圾回收從而引發(fā)終結(jié)器處理線程,此時Task的未捕獲異常會被拋出。例如:
//在Task中拋出異常 Task.Factory.StartNew(() => {throw new Exception(); }); //確保任務(wù)完成 Thread.Sleep(100); //強制垃圾回收 GC.Collect(); //等待終結(jié)器處理 GC.WaitForPendingFinalizers();好了,異常拋出,程序崩潰了。不過這個行為在.NET 4.5中又有所改變,直接運行這個程序并不會拋出異常,而在App.config中添加如下配置以后,異常才會拋出:
<configuration><runtime><ThrowUnobservedTaskExceptions enabled="true"/></runtime> </configuration>拋出異常,程序崩潰并不是程序員想要的行為,我們期望的是可以捕獲異常并處理之。要達(dá)到這個目的,針對Task對象,我們可以采用的手段有這么幾個:調(diào)用Task.Wait/WaitAll,或者引用Task<T>.Result屬性(這個在上面的例子中已經(jīng)使用了),或者最簡單的引用Task.Exception屬性來捕獲Task的異常。
例如通過Task.Wait手動捕獲AggregateException:
try {Task.WaitAll(Task.Factory.StartNew(() =>{throw new Exception();})); } catch (AggregateException) {// 處理異常//...... }這樣我們就捕獲到了異常并可以處理它了。
當(dāng)然最簡單的就是直接引用一下Task.Exception屬性:
Task.Factory.StartNew(() => {throw new Exception(); }).ContinueWith(t => { var exp = t.Exception;// 處理異常//...... });同樣的,我們捕獲了異常,并且處理掉異常就可以了,像上面例子中的處理方式就是忽略線程異常,沒做任何處理。
另外,可以通過TaskContinuationOptions.OnlyOnFaulted來使得只有在發(fā)生異常時才去執(zhí)行ContinueWith中指定的行為,代碼如下:
Task.Factory.StartNew(() => {throw new Exception(); }).ContinueWith(t => { var exp = t.Exception; }, TaskContinuationOptions.OnlyOnFaulted);最后需要說明的是TaskScheduler.UnobservedTaskException事件,該事件是所有未捕獲被拋出前的最后可以將其捕獲的方法。通過UnobservedTaskExceptionEventArgs.SetObserved方法來將異常標(biāo)記為已捕獲。
TaskScheduler.UnobservedTaskException += (s, e) => {//設(shè)置所有未捕獲異常被捕獲e.SetObserved(); };Task.Factory.StartNew(() => {throw new Exception(); });? 好了,Task的有關(guān)問題就總結(jié)到這里了,下面將總結(jié)一下并行計算方面的知識,它們與Task對象之間其實存在著千絲萬縷的聯(lián)系。
總結(jié)
以上是生活随笔為你收集整理的C#的变迁史04 - C# 4.0 之多线程篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux日志安全分析技巧
- 下一篇: 魅族19快充方案曝光:65W还是100W