26计算限制的异步操作01-CLR
由CLR via C#(第三版) ,摘抄記錄...
異步優(yōu)點:在GUI應用程序中保持UI可響應性,以及多個CPU縮短一個耗時計算所需的時間。
1、CLR線程池基礎:為提高性能,CLR包含了代碼來管理他自己的線程池--線程的集合。每CLR一個線程池,這個線程池就由CLR控制的所有appDomain共享。如果你進程中有多個CLR,就有多個線程池。
CLR初始化時,池空,線程池維護一個操作請求隊列。應用調(diào)用方法執(zhí)行異步,將一個記錄項(entry)追加到線程池的隊列。線程池從隊列提取記錄項,派遣(dispatch)給一個線程池線程,如沒有,則創(chuàng)建一個新線程。完成任務后線程不銷毀,在線程池空閑等待響應另一個請求,這樣提高性能。 當請求速度超過處理速度,就會創(chuàng)建額外線程。如果請求停止,線程空閑一段時間后,會自己醒來終止自己以釋放資源。 線程池是啟發(fā)式的,由任務多少,和可用CPU的多少,創(chuàng)建線程。
在內(nèi)部,線程池將自己的線程分為 工作者(Worker)線程和I/0線程。
2、簡單的計算限制操作
將一個異步的、計算限制的操作放到一個線程池的隊列中,通??梢哉{(diào)用ThreadPool類定義的以下方法之一:
//將方法排入隊列以便執(zhí)行。此方法在有線程池線程變得可用時執(zhí)行。static Boolean QueueUserWorkItem(WaitCallback callBack);//將方法排入隊列以便執(zhí)行,并指定包含該方法所用數(shù)據(jù)的對象。此方法在有線程池線程變得可用時執(zhí)行。static Boolean QueueUserWorkItem(WaitCallback callBack,Object state);~~~~
模擬程序 1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("Main thread: queuing an asynchronous operation"); 6 ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); 7 Console.WriteLine("Main thread: Doing other work here..."); 8 Thread.Sleep(10000); // 模擬其它工作 (10 秒鐘) 9 //Console.ReadLine(); 10 } 11 12 // 這是一個回調(diào)方法,必須和WaitCallBack委托簽名一致 13 private static void ComputeBoundOp(Object state) 14 { 15 // 這個方法通過線程池中線程執(zhí)行 16 Console.WriteLine("In ComputeBoundOp: state={0}", state); 17 Thread.Sleep(1000); // 模擬其它工作 (1 秒鐘) 18 19 // 這個方法返回后,線程回到線程池,等待其他任務 20 } 21 } 線程池01
? ?如果回調(diào)方法有異常,CLR會終止進程。
? ?3、 執(zhí)行上下文 ? ?每個線程都關(guān)聯(lián)了一個執(zhí)行上下文數(shù)據(jù)結(jié)構(gòu)。執(zhí)行上下文(execution context)包括的東西有:
- 安全設置:壓縮棧、Thread的Principal屬性[指示線程的調(diào)度優(yōu)先級]和Windows身份;
- 宿主設置:參見System.Threading.HostExecutionContextManager[提供使公共語言運行時宿主可以參與執(zhí)行上下文的流動(或移植)的功能];
- 邏輯調(diào)用上下文數(shù)據(jù):參見System.Runtime.Remoting.Messaging.CallContext[提供與執(zhí)行代碼路徑一起傳送的屬性集]的LogicalSetData[將一個給定對象存儲在邏輯調(diào)用上下文中并將該對象與指定名稱相關(guān)聯(lián)]和LogicalGetData[從邏輯調(diào)用上下文中檢索具有指定名稱的對象]。
線程執(zhí)行代碼時,有的操作會受到線程的執(zhí)行上下文設置(尤其是安全設置)的影響。理想情況下,每當一個線程(初始線程)使用另一個線程(輔助線程)執(zhí)行任務時,前者的執(zhí)行上下文應該"流動"(復制)到輔助線程。這就確保輔助線程執(zhí)行的任何操作使用的都是相同的安全設置和宿主設置。還確保了初始線程的邏輯調(diào)用上下文可以在輔助線程中使用。
默認情況下,CLR自動造成初始線程的執(zhí)行上下文會"流動"(復制)到任何輔助線程。這就是將上下文信息傳輸?shù)捷o助線程,但這對損失性能,因為執(zhí)行上下文中包含大量信息,而收集這些信息,再將這些信息復制到輔助線程,要耗費不少時間。如果輔助線程又采用更多的輔助線程,還必須創(chuàng)建和初始化更多的執(zhí)行上下文數(shù)據(jù)結(jié)構(gòu)。
System.Threading命名空間中有一個ExecutionContext類[管理當前線程的執(zhí)行上下文],它允許你控制線程的執(zhí)行上下文如何從一個線程"流動"(復制)到另一個線程。下面展示了這個類的樣子:
1 public sealed class ExecutionContext : IDisposable, ISerializable 2 { 3 [SecurityCritical] 4 //取消執(zhí)行上下文在異步線程之間的流動 5 public static AsyncFlowControl SuppressFlow(); 6 //恢復執(zhí)行上下文在異步線程之間的流動 7 public static void RestoreFlow(); 8 //指示當前是否取消了執(zhí)行上下文的流動。 9 public static bool IsFlowSuppressed(); 10 11 //不常用方法沒有列出 12 } ExecutionContext可用這個類阻止一個執(zhí)行上下文的流動,從而提升應用程序的性能。對于服務器應用程序,性能的提升可能非常顯著。但是,客戶端應用程序的性能提升不了多少。另外,由于SuppressFlow方法用[SecurityCritical]attribute進行了標識,所以在某些客戶端應用程序(比如Silverlight)中是無法調(diào)用的。當然,只有在輔助線程不需要或者不防問上下文信息時,才應該組織執(zhí)行上下文的流動。如果初始線程的執(zhí)行上下文不流向輔助線程,輔助線程會使用和它關(guān)聯(lián)起來的任何執(zhí)行上下文。在這種情況下,輔助線程不應該執(zhí)行要依賴于執(zhí)行上下文狀態(tài)(比如用戶的Windows身份)的代碼。
注意:添加到邏輯調(diào)用上下文的項必須是可序列化的。對于包含了邏輯調(diào)用上下文數(shù)據(jù)線的一個執(zhí)行上下文,如果讓它流動,可能嚴重損害性能,因為為了捕捉執(zhí)行上下文,需對所有數(shù)據(jù)項進行序列化和反序列化。
下例展示了向CLR的線程池隊列添加一個工作項的時候,如何通過阻止執(zhí)行上下文的流動來影響線程邏輯調(diào)用上下文中的數(shù)據(jù):
1 static void Main(string[] args) 2 { 3 // 將一些數(shù)據(jù)放到Main線程的邏輯調(diào)用上下文中 4 CallContext.LogicalSetData("Name", "Jeffrey"); 5 6 // 線程池能訪問到邏輯調(diào)用上下文數(shù)據(jù),加入到程序池隊列中 7 ThreadPool.QueueUserWorkItem( 8 state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); 9 10 11 // 現(xiàn)在阻止Main線程的執(zhí)行上下文流動 12 ExecutionContext.SuppressFlow(); 13 14 //再次訪問邏輯調(diào)用上下文的數(shù)據(jù) 15 ThreadPool.QueueUserWorkItem( 16 state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); 17 18 //恢復Main線程的執(zhí)行上下文流動 19 ExecutionContext.RestoreFlow(); 20 } 執(zhí)行上下文的阻止會得到一下結(jié)果:
? Name=Jeffrey
? Name=
雖然現(xiàn)在我們討論的是調(diào)用ThreadPool.QueueUserWorkItem時阻止執(zhí)行上下文的流動,但在使用Task對象(參見26.5節(jié)”任務“),以及在發(fā)起異步I/O操作(參見第27章“I/o限制的異步操作”)時,這個技術(shù)也會用到。
4、協(xié)作式取消 ?標準的取消模式,協(xié)作的,想取消的操作必須顯式地支持取消。為長時間運行的的計算限制操作添加取消能力。
首先,先解釋一下FCL提供的兩個主要類型,它們是標準協(xié)作式取消模式的一部分。
為了取消一個操作,首先必須創(chuàng)建一個System.Thread.CancellationTokenSource[通知?CancellationToken,告知其應被取消]對象。這個類如下所示: 1 public class CancellationTokenSource : IDisposable 2 { 3 //構(gòu)造函數(shù) 4 public CancellationTokenSource(); 5 //獲取是否已請求取消此 System.Threading.CancellationTokenSource 6 public bool IsCancellationRequested { get; } 7 //獲取與此 System.Threading.CancellationTokenSource 關(guān)聯(lián)的 System.Threading.CancellationToken 8 public CancellationToken Token; 9 //傳達取消請求。 10 public void Cancel(); 11 //傳達對取消的請求,并指定是否應處理其余回調(diào)和可取消操作。 12 public void Cancel(bool throwOnFirstException); 13 ... 14 } CancellationTokenSource這個對象包含了管理取消有關(guān)的所有狀態(tài)。構(gòu)造好一個CancellationTokenSource(引用類型)之后,可以從它的Token屬性獲得一個或多個CancellationToken(值類型)實例,并傳給你的操作,使那些操作可以取消。以下是CancellationToken值類型最有用的一些成員:
1 public struct CancellationToken //一個值類型 2 { 3 //獲取此標記是否能處于已取消狀態(tài),IsCancellationRequested 由非通過Task來調(diào)用(invoke)的一個操作調(diào)用(call) 4 public bool IsCancellationRequested { get; } 5 //如果已請求取消此標記,則引發(fā) System.OperationCanceledException,由通過Task來調(diào)用的操作調(diào)用 6 public void ThrowIfCancellationRequested(); 7 //獲取在取消標記時處于有信號狀態(tài)的 System.Threading.WaitHandle,取消時,WaitHandle會收到信號 8 public WaitHandle WaitHandle { get; } 9 //返回空 CancellationToken 值。 10 public static CancellationToken None 11 //注冊一個將在取消此 System.Threading.CancellationToken 時調(diào)用的委托。省略了簡單重載版本 12 public CancellationTokenRegistration Register(Action<object> callback, object state, bool useSynchronizationContext); 13 14 //省略了GetHashCode、Equals成員 15 } CancellationToken?CancellationToken實例是一個輕量級的值類型,它包含單個私有字段:對它的CancellationTokenSource對象的一個引用。在一個計算限制操作的循環(huán)中,可以定時調(diào)用CancellationToken的IsCancellationRequested屬性,了解循環(huán)是否應該提前終止,進而終止計算限制的操作。當然,提前終止的好處在于,CPU不再需要把時間浪費在你對其結(jié)果已經(jīng)不感興趣的一個操作上?,F(xiàn)在,用一些示例代碼演示一下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 CancellationTokenSource cts = new CancellationTokenSource(); 6 7 // 將CancellationToken和"要循環(huán)到的目標數(shù)"傳入操作中 8 ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000)); 9 10 Console.WriteLine("Press <Enter> to cancel the operation."); 11 Console.ReadLine(); 12 cts.Cancel(); // 如果Count方法已返回,Cancel沒有任何效果 13 // Cancel立即返回,方法從這里繼續(xù)運行 14 15 Console.ReadLine(); 16 } 17 18 private static void Count(CancellationToken token, Int32 countTo) 19 { 20 for (Int32 count = 0; count < countTo; count++) 21 { 22 //判斷是否接收到了取消任務的信號 23 if (token.IsCancellationRequested) 24 { 25 Console.WriteLine("Count is cancelled"); 26 break; // 退出循環(huán)以停止操作 27 } 28 29 Console.WriteLine(count); 30 Thread.Sleep(200); // 出于演示浪費一點時間 31 } 32 Console.WriteLine("Count is done"); 33 } 34 } 取消示例 注意:如果要執(zhí)行一個操作,并禁止取消它,可以向該操作傳遞通過調(diào)用CancellationToken的靜態(tài)None屬性返回的CancellationToken。 如果愿意,可以登記一個或多個方法,在取消一個CancellationTokenSource時調(diào)用。每個回調(diào)方法都用CancellactionToken的Register方法來登記的。要向這個方法傳遞一個Action<Object>委托;一個要通過委托傳給回調(diào)的狀態(tài);以及一個Boolean值(名為useSynchronizationContext),該值指定了是否要使用調(diào)用線程的SynchronizationContext來調(diào)用委托。如果為useSynchronizationContext參數(shù)傳遞的是false,那么調(diào)用Cancel的線程會順序調(diào)用已登記的所有方法。如果為true,那么回調(diào)會被send(而不是post)給已捕捉的SynchronizationContext對象,后者決定由哪個線程調(diào)用回調(diào)方法。 說明:如果執(zhí)行send操作,要等到目標線程那里處理完畢之后才會返回。再次期間,調(diào)用線程會被阻塞。這相當于同步調(diào)用。而如果執(zhí)行post操作,是指將東西post到一個隊列中便完事,調(diào)用線程可以立即返回。相當于異步調(diào)用。以后會詳細提到。 如果多次調(diào)用Regiser,那么多個回調(diào)方法都會調(diào)用。這些回調(diào)方法可能拋出未處理的異常。如果調(diào)用CancellationTokenSource的Cancel方法,向它傳遞true,那么拋出了未處理異常的第一個回調(diào)方法會組織其他回調(diào)方法的執(zhí)行,拋出的異常也會從Cancel中拋出。如果調(diào)用Cancel并向它傳遞false,那么登記的所有回調(diào)方法都會調(diào)用。所有未處理的異常都會添加到一個集合中。所有回調(diào)方法都執(zhí)行后,如果其中任何一個拋出一個未處理的異常,Cancel就會排除一個AggregateException,該異常實例的InnerException屬性會被設為以拋出的所有異常對象的一個集合。如果以等級的所有回調(diào)方法都沒有拋出異常,那么Cancel直接返回,不拋出任何異常。 注:沒辦法把InnerExceptions的一個異常與特定操作對應起來。需查看StackTrace屬性并手動檢查代碼。 CancellactionToken的Register方法返回一個CancellationTokenRegistration,如下所示: public struct CancellationTokenRegistration : IEquatable<CancellationTokenRegistration>, IDisposable {public void Dispose();....... }可調(diào)用Dispose從關(guān)聯(lián)的CancellationTokenSource中刪除一個已登記的回調(diào);這樣一來,在調(diào)用Cancel時,便不會再調(diào)用這個回調(diào)。以下代碼演示了如何向一個CancellationTokenSource登記兩個回調(diào):
private static void Register() {var cts = new CancellationTokenSource();cts.Token.Register(() => Console.WriteLine("Canceled 1"));cts.Token.Register(() => Console.WriteLine("Canceled 2"));// 出于測試目的,讓我們?nèi)∠?#xff0c;以便執(zhí)行兩個回調(diào)cts.Cancel(); }可通過鏈接另一組的CancellationTokenSource來新建一個CancellationTokenSource對象。任何一個鏈接的CancellationTokenSource被取消,這個CancellationTokenSource對象就會被取消。以下代碼對此進行的演示:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 // 創(chuàng)建一個 CancellationTokenSource 6 var cts1 = new CancellationTokenSource(); 7 cts1.Token.Register(() => Console.WriteLine("cts1 canceled")); 8 9 // 創(chuàng)建另一個 CancellationTokenSource 10 var cts2 = new CancellationTokenSource(); 11 cts2.Token.Register(() => Console.WriteLine("cts2 canceled")); 12 13 // 創(chuàng)建新的CancellationTokenSource,它在 cts1 o或 ct2 is 取消時取消 14 var ctsLinked = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); 15 ctsLinked.Token.Register(() => Console.WriteLine("linkedCts canceled")); 16 17 // 取消其中一個 CancellationTokenSource objects (這里選擇了 cts2) 18 cts2.Cancel(); 19 20 // 顯示哪個 CancellationTokenSource objects 被取消 了 21 Console.WriteLine("cts1 canceled={0}, cts2 canceled={1}, ctsLinked canceled ={2}", 22 cts1.IsCancellationRequested, cts2.IsCancellationRequested, ctsLinked.IsCancellationRequested); 23 24 Console.ReadLine(); 25 } 26 27 } 鏈接式的取消?
5、任務?調(diào)用ThreadPool的QueueUserWorkItem方法來發(fā)起一次異步的受計算限制的操作是非常簡單的。然而。這個技術(shù)存在許多限制。最大的問題是沒有一個內(nèi)建的機制讓你知道操作在什么時候完成,也沒有一個機制在操作完成時獲得一個返回值。為了克服這些限制并解決一些其它問題,Microsoft引入了任務(task)的概念。我們通過System.Treading.Tasks命名空間中的類型來使用它們。
現(xiàn)在,我們可以使用任務來做相同的事情: ThreadPool.QueueUserWorkItem(ComputeBoundOp,5) // 調(diào)用QueueUserWorkItemnew Task(ComputeBoundOp,5).Start(); // 用Task來做相同的事情 在上述代碼中,創(chuàng)建了Task對象,并立即調(diào)用Start方法來調(diào)度該任務方法。當然,也可以先創(chuàng)建好Task對象,以后在調(diào)用Start方法。把Task對象傳遞給一個方法,由后者決定執(zhí)行時間和頻率。 創(chuàng)建一個Task的方式總是調(diào)用構(gòu)造器,傳遞一個Action或者Action<Object>委托。這個委托就是你想執(zhí)行的操作。如果傳遞期待一個Object的方法,還必須向Task的構(gòu)造器傳遞最終想傳給操作的實參。還可以選擇向Task的構(gòu)造器傳遞一個CancellationToken,這便允許Task在調(diào)度之前取消。 還可以選擇向構(gòu)造器傳遞一些TaskCreationOptions標志來控制Task的執(zhí)行方式。TaskCreationOptions是一個枚舉類型,定義了一組可按位OR到一起的標志。它的定義如下: 1 [FlagsAttribute, SerializableAttribute] 2 public enum TaskCreationOptions 3 { 4 //指定應使用默認行為 5 None = 0x0, 6 //提示 TaskScheduler 以一種盡可能公平的方式安排任務,這意味著較早安排的任務將更可能較早運行,而較晚安排運行的任務將更可能較晚運行。造成默認的TaskScheduler(任務調(diào)度器) 將線程池中的任務放到全局隊列中,而不是放到一個工作者線程的本地隊列中 7 PreferFairness = 0x1, 8 //指定某個任務將是運行時間長、粗粒度的操作。 它會給TaskScheduler一個提議,告訴它線程可能要“長時間運行”,將由TaskScheduler 決定如何解析還這個提示。 9 LongRunning = 0x2, 10 //將一個任務和它的父Task關(guān)聯(lián)。 11 AttachedToParent = 0x4, 12 #if NET_4_5 13 // 14 DenyChildAttach = 0x8, 15 HideScheduler = 0x10 16 #endif 17 } TaskCreationOptions大多是標志只是一些提議而已,TaskScheduler在調(diào)度一個Task時,可能會也可能不會采納這些提議。不過,AttacedToParent標志總是得到采納,因為它和TaskScheduler本身無關(guān)。
? ?
5.1 等待任務完成并獲取它的結(jié)果
對于任務,可以等待它完成,然后獲取它們的結(jié)果。假定有一個Sum方法,在n值很大的時候,它要執(zhí)行較長的時間: private static Int32 Sum(Int32 n) {Int32 sum = 0;for (; n > 0; n--) checked { sum += n; } //如果n太大,這一行代碼會拋出異常return sum;} 現(xiàn)在可以構(gòu)造一個Task<TResult>對象(派生自Task),并為泛型TResult參數(shù)傳遞計算限制操作的返回類型。在開始任務后,可以等待它完成并獲取結(jié)果,如以下代碼所示: 1 class Program 2 { 3 static void Main(string[] args) 4 { 5 // 創(chuàng)建 Task, 推遲啟動它 6 Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 10000); 7 8 // 可以在以后某個時間啟動任務 9 t.Start(); 10 11 // 可以選擇顯式的等待任務完成 12 t.Wait(); 13 14 Console.WriteLine("The sum is: " + t.Result); //一個Int32的值 15 Console.ReadLine(); 16 } 17 18 private static Int32 Sum(Int32 n) 19 { 20 Int32 sum = 0; 21 for (; n > 0; n--) checked { sum += n; } //如果n太大,這一行代碼會拋出異常 22 return sum; 23 } 24 25 } Task01 如果計算限制的任務拋出一個未處理的異常,這個一樣會被"侵吞"并存儲到一個集合中,而線程池線程允許返回到線程池中。調(diào)用Wait方法或者Result屬性時,這些成員會拋出一個System.AggregateException對象。 提示:一個線程調(diào)用Wait方法時,系統(tǒng)會檢查系統(tǒng)要等待的Task是否已開始執(zhí)行。如果是,調(diào)用Wait的線程會阻塞,直到Task運行結(jié)束為止。但是,如果Task還沒有開始執(zhí)行,系統(tǒng)可能(取決于TaskSecheduler)使用調(diào)用Wait的線程來執(zhí)行Task。如果發(fā)生這種情況,那么調(diào)用Wait的線程不會阻塞;它會執(zhí)行Task并立即返回。這樣做的好處在于,沒有線程會被阻塞,所以減少了資源的使用(因為不需要創(chuàng)建一個線程來替代被阻塞的線程),并提升了性能(因為不需要花時間創(chuàng)建一個線程,也沒有上下文切換)。但是不好的地方在于,加入線程在調(diào)用Wait前已經(jīng)獲得了一個線程同步鎖,而Task試圖獲取同一個鎖,就會造成一個死鎖的線程。 AggregateException類型用于封閉異常對象的一個集合(如果父任務生成了多個字任務,而多個子任務都拋出異常,這個集合便有可能包含多個異常)。該類型有一個InnerExceptions屬性,它返回一個ReadOnlyCollection<Excepyion>對象。不要混淆InnerException和InnerException屬性,后者是AggregateException類從System.Exception基類繼承來的。對于上例來說,AggregateException的InnerExceptions屬性的元素0將引用由計算限制方法(Sum)拋出的實際System.OverflowException對象。 為方便編碼,AggregateException重寫了Exception的GetBaseException方法。AggregateException的這個實現(xiàn)會返回作為問題根源的最內(nèi)層的AggregateException。AggregateException還提供了一個Flatten方法,它創(chuàng)建一個新的AggregateException,其InnerExceptions屬性包含一個異常列表,其中的異常是通過遍歷原始AggregateException的內(nèi)層異常層次結(jié)構(gòu)而生成的。最后,AggregateException還提供了一個Handle方法,它為AggregateException中包含的每個異常都調(diào)用一個回調(diào)方法,然后,回調(diào)方法可以為每個在調(diào)用Handle之后,如果至少有一個異常沒有處理,就創(chuàng)建一個新的AggregateException對象,其中只包含未處理的異常,并拋出這個新的AggregateException對象。? ? Task還提供等待任務數(shù)組。WaitAny是任意任務完成就返回,不再阻塞。反饋的是完成任務在數(shù)組中的下標,若超時則返回-1。
?類似的,Task類還提供了靜態(tài)WaitAll方法,它阻塞調(diào)用線程,直到數(shù)組中所有的Task對象都完成。如果Task對象都完成,WaitAll方法返回true。如果?發(fā)生超時,就返回false。
如果WaitAny、WaitAll通過一個CancellationToken而取消,會拋出一個OpreationCanceledException。
如果一直不調(diào)用 Wait獲Result,或者一直不查詢Task的Exception,就可能忽略一些異常,當Task對象的Finalize執(zhí)行時,會拋出異常,這是CLR終結(jié)器拋出的,不能捕捉,進程會立即終止。(CLR第三版的 26.5.1章節(jié)提到一種處理,見P648)
5.2取消任務??可以用一個CancellationTokenSource取消一個Task.
private static Int32 Sum(CancellationToken ct, Int32 n){Int32 sum = 0;for (; n > 0; n--){// 在取消標志引用的CancellationTokenSource上如果調(diào)用Cancel,// 下面這一行就拋出OpreationCanceledExceptionct.ThrowIfCancellationRequested();checked { sum += n; } //如果n太大,這一行代碼會拋出異常}return sum;}在上述代碼中,在計算限制的循環(huán)中,我們通過調(diào)用CancellationToken的ThrowIfCancellationRequested方法來定時檢查操作是否已取消。這個方法和CancellationToken的IsCancellationRequested屬性相似。如果CancellationTokenSource已經(jīng)取消,ThrowIfCancellationRequested會拋出一個OpreationCanceledException。之所以選擇拋出一個異常,是因為有別于ThreadPool的QueueUserWorkItem方法所初始化的工作項(work item),任務有表示已經(jīng)完成的方法,甚至還有一個返回值。所以,需要采取一種方式將已完成的任務和出錯的任務區(qū)分開。而讓任務拋出一個異常,就可以知道任務沒有一直運行到結(jié)束。 現(xiàn)在,我們像下面這樣創(chuàng)建CancellationTokenSource和Task對象: 1 static void Main(string[] args) 2 { 3 CancellationTokenSource cts = new CancellationTokenSource(); 4 Task<Int32> t = new Task<Int32>(() => Sum(cts.Token, 10000), cts.Token); 5 6 t.Start(); 7 8 // 在之后的某個時間,取消CancellationTokenSource以取消Task 9 cts.Cancel(); 10 11 try 12 { 13 // 如果任務已經(jīng)取消,Result會拋出一個AggregateException 14 Console.WriteLine("The sum is: " + t.Result); // An Int32 value 15 } 16 catch (AggregateException ae) 17 { 18 // 將任何OperationCanceledException對象都視為已處理 19 // 其他任何異常都造成拋出一個新的AggregateException,其中 20 // 只包含未處理的異常 21 ae.Handle(e => e is OperationCanceledException); 22 23 // 所有的異常都處理好之后,執(zhí)行下面這一行 24 Console.WriteLine("Sum was canceled"); 25 } 26 Console.ReadLine(); 27 } 可取消Task示例 創(chuàng)建一個Task時,可以將一個CancellationToken傳給Task的構(gòu)造器,從而將這個CancellationToken和該Task關(guān)聯(lián)起來。如果CancellationToken在Task調(diào)度前取消,Task會被取消,永遠不會執(zhí)行。但是,如果Task已經(jīng)調(diào)度,那么Task為了允許它的操作在執(zhí)行期間取消,Task的代碼必須顯式支持取消。遺憾的是,雖然Task對象關(guān)聯(lián)了一個CancellationToken,但沒有辦法訪問它。因此,必須通過某種方式,在Task的代碼本身中獲得用于創(chuàng)建Task對象的同一個CancellationToken。為了寫這樣的代碼,最簡單的方法就是使用一個lambda表達式,并將CancellationToken作為一個閉包變量"傳遞"(就像上例所示)。 5.3?一個任務完成時自動啟動一個新任務
? ?? 要寫可伸縮的軟件,一定不能使你的線程阻塞。這意味著如果調(diào)用Wait,或者在任何尚未完成時查詢?nèi)蝿盏腞esult屬性(Result內(nèi)部會調(diào)用Wait),極有可能造成線程池創(chuàng)建一個新線程,這增大了資源的消耗,并損害了伸縮性。幸好,有更好的方式知道一個任務在上面時候結(jié)束運行。一個任務完成時,它可以啟動另一個任務。下面重寫了前面的代碼,它不會阻塞線程:
? ??
static void Main(string[] args){// 創(chuàng)建 Task, 推遲啟動它, 繼續(xù)另一個任務Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 10000);// 可以在以后某個時間啟動任務t.Start();// ContinueWith 返回一個 Task 但一般不再關(guān)心這個對象Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result));cwt.Wait();Console.ReadLine();}現(xiàn)在,當執(zhí)行Sum的任務完成后,這個任務會啟動另一個任務(也在某個線程池線程上)以顯示結(jié)果。執(zhí)行上述代碼的線程不會進入阻塞狀態(tài)并等待這個兩個任務中的任何一個完成。相反,線程可以執(zhí)行其它代碼。如果線程線程本身就是線程池線程,它可以返回到池中,以執(zhí)行其他操作。注意,執(zhí)行Sum的任務可能在調(diào)用ContinueWith之前完成。但這不是一個問題,因為ContinueWith方法會看到Sum任務已經(jīng)完成,會立即啟動顯示結(jié)果的任務。 另外,注意ContinueWith會返回新的Task對象的一個引用。當然,可以用這個Task對象調(diào)用各種成員(比如Wait,Result,甚至ContinueWith),但你一般都是忽略這個Task對象,不把它的引用保存到一個變量中。 Task對象內(nèi)部包含了Continue任務的一個集合。所以,實際上可以用一個Task對象來多次調(diào)用ContinueWith。任務完成時,所有ContinueWith任務都會進入線程池的隊列中。此外,調(diào)用ContinueWith時,可以傳遞對一組TaskContinuationOptions枚舉值進行按位OR運行的結(jié)果。前4個標志(None,PreferFairness,LongRunning和AttachToParent)與早先描述的TaskCreationOptions枚舉類型提供的標志完全一致,下面是TaskContinuationOptions類型的定義: 1 [System.FlagsAttribute, System.SerializableAttribute] 2 public enum TaskContinuationOptions 3 { 4 None = 0x00000, 5 PreferFairness = 0x00001, 6 LongRunning = 0x00002, 7 AttachedToParent = 0x00004, 8 #if NET_4_5 9 DenyChildAttach = 0x00008, 10 HideScheduler = 0x00010, 11 LazyCancellation = 0x00020, 12 #endif 13 //指定不應在延續(xù)任務前面的任務已完成運行的情況下安排延續(xù)任務。 此選項對多任務延續(xù)無效。 14 NotOnRanToCompletion = 0x10000, 15 //指定不應在延續(xù)任務前面的任務引發(fā)了未處理異常的情況下安排延續(xù)任務。 此選項對多任務延續(xù)無效。 16 NotOnFaulted = 0x20000, 17 //指定不應在延續(xù)任務前面的任務已取消的情況下安排延續(xù)任務。 此選項對多任務延續(xù)無效。 18 NotOnCanceled = 0x40000, 19 //指定只應在延續(xù)任務前面的任務已完成運行的情況下才安排延續(xù)任務。 此選項對多任務延續(xù)無效。 20 OnlyOnRanToCompletion = 0x60000, 21 //指定只有在延續(xù)任務前面的任務引發(fā)了未處理異常的情況下才應安排延續(xù)任務。 此選項對多任務延續(xù)無效。 22 OnlyOnFaulted = 0x50000, 23 //指定只應在延續(xù)任務前面的任務已取消的情況下安排延續(xù)任務。此選項對多任務延續(xù)無效。 24 OnlyOnCanceled = 0x30000, 25 //指定應同步執(zhí)行延續(xù)任務。 指定此選項后,延續(xù)任務將在導致前面的任務轉(zhuǎn)換為其最終狀態(tài)的相同線程上運行。 如果在創(chuàng)建延續(xù)任務時已經(jīng)完成前面的任務,則延續(xù)任務將在創(chuàng)建此延續(xù)任務的線程上運行。 只應同步執(zhí)行運行時間非常短的延續(xù)任務。 26 ExecuteSynchronously = 0x80000, 27 } TaskContinuationOptions
調(diào)用COntinueWith時,可以指定你希望新任務只有在第一個任務被取消時才運行,這時使用TaskContinuationOptions.?OnlyOnCanceled標志來實現(xiàn)。默認情況下,如果沒有指定上述任何標志,新任務無論如何都會執(zhí)行下去,不管第一個任務是如何完成的。一個Task完成時,它的所有尚未運行的延續(xù)任務都會自動取消。下面用一個例子演示所有這些概念。
1 static void Main(string[] args) 2 { 3 Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 10000); 4 5 t.Start(); 6 7 // 每個 ContinueWith 都返回一個 Task,但你不必關(guān)心這些Task對象 8 t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result), 9 TaskContinuationOptions.OnlyOnRanToCompletion); 10 t.ContinueWith(task => Console.WriteLine("Sum threw: " + task.Exception), 11 TaskContinuationOptions.OnlyOnFaulted); 12 t.ContinueWith(task => Console.WriteLine("Sum was canceled"), 13 TaskContinuationOptions.OnlyOnCanceled); 14 15 Console.ReadLine(); 16 17 }?5.4任務啟動子任務 ??
最后,任務支持父/子關(guān)系,如下代碼所示: static void Main(string[] args){Task<Int32[]> parent = new Task<Int32[]>(() =>{var results = new Int32[3]; // 創(chuàng)建數(shù)組來存儲結(jié)果// 這個任務創(chuàng)建并啟用了3個子任務new Task(() => results[0] = Sum(10000), TaskCreationOptions.AttachedToParent).Start();new Task(() => results[1] = Sum(20000), TaskCreationOptions.AttachedToParent).Start();new Task(() => results[2] = Sum(30000), TaskCreationOptions.AttachedToParent).Start();// 返回對數(shù)組的一個引用(即使數(shù)組元素可能還沒有初始化)return results;});var cwt = parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));parent.Start();Console.ReadLine();} 在前面例子中,父任務創(chuàng)建并啟用3個Task對象。默認情況下,一個任務創(chuàng)建的Task對象是頂級任務,這些任務與創(chuàng)建它們的那個任務無關(guān)。然而,TaskContinuationOptions.?AttachedToParent 標志將一個Task和創(chuàng)建它的那個Task關(guān)聯(lián)起來,結(jié)果是除非所有子任務結(jié)束運行,否則創(chuàng)建任務(父任務)不會認為已經(jīng)結(jié)束。調(diào)用ContinueWith方法創(chuàng)建一個Task時,可以指定TaskContinuationOptions.?AttachedToParent 標志將延續(xù)任務指定的一個子任務。 5.5 任務內(nèi)部解密? 每個Task對象都有一組構(gòu)成任務狀態(tài)的字段。有一個Int32ID、代表Task執(zhí)行狀態(tài)的一個Int32、對父任務的一個引用、對Task創(chuàng)建時指定的TaskScheduler的一個引用、對回調(diào)方法的一個引用、對要傳給回調(diào)方法的對象的一個引用(可通過Task的只讀AsynState屬性查詢)、對一個ExecutionContext的引用以及對一個ManualResetEventSlim對象的引用。除此之外,每個Task對象都有對根據(jù)需要創(chuàng)建一個一些補充狀態(tài)的一個引用。 在補充狀態(tài)中,包含一個CancellactionToken、一個ContinueWithTask對象集合、為拋出了未處理異常的子任務而準備的一個Task對象集合等。雖然任務提供了大量功能,但并非是沒有代價的。因為必須為所有的這些狀態(tài)分配內(nèi)存。如果不需要任務提供的附加功能,那么使用ThreadPool.QueueUserWorkItem,資源的使用效率上會更高一些。 Task和Task<TResult>類實現(xiàn)了IDisposable接口,允許你在用完Task對象后調(diào)用Dispose。如今,所有Dispose方法所做的都是關(guān)閉ManuaResetEventSlim對象。然而,可以定義從Task和Task<Result>派生的類,在這些類中分配它們自己的資源,并在它們重寫的Dispose方法中釋放這些資源。當然,大多數(shù)開發(fā)人員都不會在自己的代碼中顯式的為一個Task對象調(diào)用Dispose;他們只讓垃圾回收器回收任何不再需要的資源。 在每個Yask對象中,都包含代表Task唯一ID的一個Int32字段。創(chuàng)建一個Task對象時,字段會被初始化為零。第一次查詢Task的只讀ID屬性,屬性將一個唯一Int32值分配給該字段,并從屬性中返回它。TaskID從1開始,每分配一個ID都會遞增1.在Visual Studio調(diào)試器中查看一個Task對象,會造成調(diào)試器顯示Task的ID,從而造成為Task分配一個ID。 這個ID的意義在于,每個Task都可以用一個唯一的值來標識。事實上,Visual Studio會在它的"并行任務"和"并行堆棧"窗口中會顯示這個任務ID。但是,由于不在自己的代碼中分配ID,所以幾乎不可能將這個ID和代碼正在做的事聯(lián)系起來。運行一個任務的代碼時,可以查詢Task的靜態(tài)CurrenId屬性,它返回一個可空的Int32(Int32?)。還可以在調(diào)式期間,在Vasul Studio的"監(jiān)視"或"即時"窗口中調(diào)用它,以便獲得當前正在調(diào)試的代碼的ID。然后,可以在"并行任務"和"并行堆棧"窗口中找到自己的任務。如果當前沒有任務正在執(zhí)行,查詢CurrenId屬性會返回null。 一個Task對象存在期間,可查詢Task的只讀Status屬性了解它在其生存期的什么位置。這個屬性返回一個TaskStatus值,定義如下: public enum TaskStatus{//這些標志指出了一個Task在其生命周期內(nèi)的狀態(tài)// 任務已顯式創(chuàng)建,可以手動Start()這個任務Created,// 任務已隱式創(chuàng)建,會自動開始WaitingForActivation,// 任務已調(diào)度,但尚未運行WaitingToRun,// 任務正在運行Running,// 任務正在等待它的子任務完成,子任務完成后它才完成WaitingForChildrenToComplete,// 一個任務的最終狀態(tài)是以下三種之一// 已成功完成執(zhí)行的任務RanToCompletion,// 該任務已通過對其自身的 CancellationToken 引發(fā) OperationCanceledException 對取消進行了確認,此時該標記處于已發(fā)送信號狀態(tài);或者在該任務開始執(zhí)行之前,已向該任務的 CancellationToken 發(fā)出了信號Canceled,// 由于未處理異常的原因而完成的任務Faulted} 首先構(gòu)造一個Task對象時,它的狀態(tài)是Created。以后,任務啟動時,它的狀態(tài)變?yōu)閃aitngToRun。Task在一個線程上運行時,它的狀態(tài)就變成了Running。任務停止運行,并等待它的任何子任務時,狀態(tài)變成WaitingForChildrenToComplete。任務完全結(jié)束時,它會進入以下三種狀態(tài)的一種:RanToCompletion、Canceled或Faulted。一個Task<Result>運行完成時,可通過Task<TResult>的Result屬性來查詢?nèi)蝿盏慕Y(jié)果。一個Task或者Task<TResult>出錯時,可以查詢Task的Exception屬性來獲得任務拋出的未處理的異常:該屬性總是返回一個AggregateException對象,它包含了所有未處理的異常。 為簡化編碼,Task提供了幾個只讀的Boolean屬性:IsCanceled,IsFaulted和IsCompleted。注意,當Task處于RanToCompleted,Canceled或者Faulted狀態(tài)時,IsCompleted返回true。為了判斷一個Task是否成功完成,最簡單的辦法就是使用如下所示的代碼: if (task.Status == TaskStatus.RanToCompleted )....... 如果Task是通過調(diào)用以下某個函數(shù)來創(chuàng)建的,這個Task對象就處于WaitingForActivation狀態(tài):ContinueWith、ContinueWithAll,ContinueWithAnv或者FromAsnc。如果通過構(gòu)造一個TaskCompletionSource<TResult>對象[表示未綁定到委托的?Task<TResult>?的制造者方,并通過?Task?屬性提供對使用者方的訪問]創(chuàng)建一個Task,該Task在創(chuàng)建時也處于WaitingForActivation狀態(tài)。這個狀態(tài)意味著該Task的調(diào)度由任務基礎結(jié)構(gòu)控制。例如,不能顯式啟動一個通過ContinueWith創(chuàng)建的對象。這個Task會在它的先驅(qū)任務(antecedent task)執(zhí)行完畢后自動開始。 5.6任務工廠 ? 有的時候,可能需要創(chuàng)建一組Task任務來共享相同的狀態(tài)。為了避免機械地將相同的參數(shù)傳給每一個Task的構(gòu)造器,可以創(chuàng)建一個任務工廠來封裝通用的狀態(tài),System.Threding.Tasks命名空間定義了一個TaskFactory類型和一個TaskFactory<TResult>類型。兩個類型都派生自System.Object;也就是說,它們是平級的。 如果要創(chuàng)建的是一組沒有返回值的任務,那么要構(gòu)造一個TaskFactory;如果要創(chuàng)建的是一組有一個特定返回值的任務,那么要構(gòu)造一個TaskFactory<TResult>,并通過泛型TResult實參來傳遞任務的返回類型。創(chuàng)建任何任務工廠類時,要向它的構(gòu)造器傳遞一些默認值。工廠創(chuàng)建的任務都將具有這些默認值。具體的說,要想工廠傳遞你希望工廠創(chuàng)建的任務具有的CancellationToken,TaskScheduler,TaskCreationOptions和TaskContinuationOptions設置。 以下實例代碼演示了如何使用一個TaskFatory: 1 private static void Test5() 2 { 3 var parent = new Task(() => 4 { 5 var cts = new CancellationTokenSource(); 6 var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); 7 8 // 這個任務創(chuàng)建并啟動三個子任務 9 var childTasks = new[] { 10 tf.StartNew(() => Sum(cts.Token, 10000)), 11 tf.StartNew(() => Sum(cts.Token, 20000)), 12 tf.StartNew(() => Sum(cts.Token, Int32.MaxValue)) , // 太大,拋出 OverflowException異常 13 tf.StartNew(() => Sum(cts.Token, 30000)) 14 }; 15 16 // 如果子任務拋出異常就取消其余子任務 17 for (Int32 task = 0; task < childTasks.Length; task++) 18 childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted); 19 20 // 所有子任務完成后,從未出錯/未取消的任務返回的值, 21 // 然后將最大值傳給另一個任務來顯示結(jié)果 22 tf.ContinueWhenAll(childTasks, 23 completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result), 24 CancellationToken.None) 25 .ContinueWith(t => Console.WriteLine("The maximum is: " + t.Result), 26 TaskContinuationOptions.ExecuteSynchronously); 27 }); 28 // 子任務完成后,也顯示任何未處理的異常 29 parent.ContinueWith(p => 30 { 31 // 將所有文本放到一個 StringBuilder 中并只調(diào)用 Console.WrteLine 一次 32 // 因為這個任務可能和上面任務并行執(zhí)行,而我不希望任務的輸出變得不連續(xù) 33 StringBuilder sb = new StringBuilder("The following exception(s) occurred:" + Environment.NewLine); 34 foreach (var e in p.Exception.Flatten().InnerExceptions) 35 sb.AppendLine(" " + e.GetType().ToString()); 36 Console.WriteLine(sb.ToString()); 37 }, TaskContinuationOptions.OnlyOnFaulted); 38 39 // 啟動父任務,便于它啟動子任務 40 parent.Start(); 41 } TaskFactoryTest 通過上述代碼,創(chuàng)建了一個TaskFactory<Int32>對象。這個任務工廠將用于創(chuàng)建3個Task對象。希望它做4件事:每個Task對象都共享相同的CancellationTokenSource.Toke,其中3個任務被視為其父任務的子任務,TaskFactory對象創(chuàng)建的所有延續(xù)任務都同步執(zhí)行,而且這個TaskFactory創(chuàng)建的所有Task對象都是用默認的TaskScheduler。 然后創(chuàng)建一個數(shù)組,其中包含了3個子Task對象,所有都是通過TaskFactory的StartNew方法來創(chuàng)建的。使用這個方法,可以方便的創(chuàng)建并啟動每個子任務。在一個循環(huán)中,告訴每個子任務,如果拋出一個未處理的異常,就會取消其它仍在運行的所有子任務。最后,在TaskFacroty上調(diào)用ContinueWithAll,它創(chuàng)建一個在所有子任務都結(jié)束后運行的一個Task。由于這個任務是用TaskFactory創(chuàng)建的,所以它仍然被視為父任務的一個子任務,會使用默認的TaskScheduler同步執(zhí)行。然而,希望即使其他子任務被取消,也要運行這個任務。因此,我傳遞CancellationToken.None來覆蓋TaskFactory的CancellationToken。這會造成該任務完全不能取消。最后,當處理所有結(jié)果的任務完成后,創(chuàng)建另一個任務來顯示從所喲子任務中返回的最大值。 注意:調(diào)用TaskFactory或TaskFactory<TRsult>的靜態(tài)ContinueWhenAll和ContinueWhenAny方法時,以下TaskContinuationOption標志是非法的:NotOnRanToComplettion,NoyOnFaulted和NotCanceled。也就是說,無論先驅(qū)任務是如何完成的,ContinueWhenAll和ContinueWhenAny方法時都會執(zhí)行延續(xù)任務。 5.7 ?任務調(diào)度器(TaskScheduler)? 任務基礎結(jié)構(gòu)是很靈活的,其中TaskScheduler對象功不可沒。TaskScheduler對象負責執(zhí)行調(diào)度的任務,同時向Visual Studio 調(diào)試器公開任務信息。FCL提供了兩個派生自TaskScheduler的類型:線程池任務調(diào)度器(thread pool task scheduler)和同步上下文任務調(diào)度器(synchronization context task scheduler)。 默認情況下,所有應用程序使用的都是線程池任務調(diào)度器。這個任務調(diào)度器將任務調(diào)度給線程池的工作者線程,將在后面進行更詳細的討論。可以查詢TaskScheduler的靜態(tài)Default屬性來獲得對默認任務調(diào)度器的一個引用。. ?同步上下文任務調(diào)度器通常用于Windows窗體、WPF和Silverlight應用程序。這個任務調(diào)度器將所有任務都調(diào)度給應用程序的GUI線程,是所有任務代碼都能成功更新UI,比如按鈕。菜單項等。同步上下文任務調(diào)度器根本不使用線程池??梢圆樵僒askScheduler的FromCurrentSynchronizationContext方法來獲取對一個同步上下文任務調(diào)度器的引用。~~~~~~待續(xù)。。。。
?
復制去Google翻譯翻譯結(jié)果轉(zhuǎn)載于:https://www.cnblogs.com/dacude/p/4391393.html
總結(jié)
以上是生活随笔為你收集整理的26计算限制的异步操作01-CLR的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: E语言基本特征码/时钟反调试/窗体pus
- 下一篇: ALC662 在Mac中的安装