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