作者:zlbcdn
協(xié)作式取消
協(xié)作式取消其英文為: cooperative cancellation model。在26.4節(jié)中只是很簡單的介紹了通過CancellationTokenSource來終結(jié)一個異步操作或長時間執(zhí)行的同步操作。沒有具體的分析和說明為什么要這樣用。因為終結(jié)一個異步操作的方法有很多,可以使用最簡單的true
和false
變量結(jié)束異步操作。因此本次詳細整理CLR的在線程取消的模式。本文參考了MSDN及其他網(wǎng)友的相關(guān)資料,具體的引用會在文章的尾端。
從.NET4開始,.NET Framework才為異步或需長時間執(zhí)行的同步操作提供了協(xié)作取消模式。通常使用的有兩個“東西“,一個是CancellationTokenSource,另一個是struct:CancellationToken。前者是取消請求的發(fā)起者,而后者是消息請求的監(jiān)聽者。就像量子世界中的量子糾纏一樣,一個是根據(jù)現(xiàn)場的環(huán)境做出相應(yīng)的響應(yīng),而另一個會立刻做出反應(yīng)。CancellationTokenSource與CancellationToken就是這樣的一個狀態(tài)。
協(xié)作式取消的使用
協(xié)作式取消的使用步驟如下:
1、創(chuàng)建CancellationTokenSource實例
2、使用CancellationTokenSource實例的Token屬性,獲取CancellationToken,并將其傳至Task或線程的相關(guān)方法中
3、在task或thread中提供根據(jù)CancellationToken.IsCancellationRequested屬性值進行判定是否應(yīng)該停止操作的機制
4、在程序中調(diào)用CancellationTokenSource實例的cancel方法
這兒有一篇文章,是使用CancellationTokenSource的具體例子。.Net 4.5中通過CancellationTokenSource實現(xiàn)對超時任務(wù)的取消
CancellationTokenSource
1、定義
CancellationTokenSource類的定義如下:
[ComVisibleAttribute(false)][HostProtectionAttribute(SecurityAction.LinkDemand, Synchronization = true, ExternalThreading = true)]public class CancellationTokenSource : IDisposable
因本類實現(xiàn)了IDisposable的方法,因此在用完時需調(diào)用其dispose方法,或者是使用using
2、CancellationTokenSource與CancellationToken的關(guān)系
兩者的關(guān)系如圖所示:
using System;using System.Threading;public class Example{ public static void Main() { // Create the token source. CancellationTokenSource cts = new CancellationTokenSource(); // Pass the token to the cancelable operation. ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token); Thread.Sleep(2500); // Request cancellation. cts.Cancel(); Console.WriteLine("Cancellation set in token source..."); Thread.Sleep(2500); // Cancellation should have happened, so call Dispose. cts.Dispose(); } // Thread 2: The listener static void DoSomeWork(object obj) { CancellationToken token = (CancellationToken)obj; for (int i = 0; i < 100000; i++) { if (token.IsCancellationRequested) { Console.WriteLine("In iteration {0}, cancellation has been requested...", i + 1); // Perform cleanup if necessary. //... // Terminate the operation. break; } // Simulate some work. Thread.SpinWait(500000); } }}// The example displays output like the following:// Cancellation set in token source...// In iteration 1430, cancellation has been requested...
以上方法使用的系統(tǒng)遺留方式,但是希望停止一個task時,參見如下:How to: Cancel a Task and Its Children
操作取消與對象取消(Operation Cancellation Versus Object Cancellation)
在協(xié)作式取消操作中,通常都是在方法中通過判斷Token的IsCancellationRequested屬性,然后根據(jù)這個屬性的值對操作(或方法)進行相應(yīng)的處理。因此,常用的協(xié)作式取消模式就是Operation Cancellation。PS.Token的IsCancellationRequested只能被設(shè)置一次,即當(dāng)該屬性被設(shè)置為true時,其不可能再被設(shè)為false,不能重復(fù)利用。另外,Token在被“用過”后,不能重復(fù)使用該對象。即,CancellationTokenSource對象只能使用一次,若希望重復(fù)使用,需要在每次使用時,創(chuàng)建新的對象。
除了操作取消之外,還有另外一種情況,我希望當(dāng)CancellationTokenSource實例調(diào)用cancel方法時,調(diào)用某個實例中的某個方法。而這個方法內(nèi)部沒有CancellationToken對象。這個時候可以使用CancellationTokenSource的Register方法。
方法的定義如下:
public CancellationTokenRegistration Register(Action callback)
其中Action是.NET內(nèi)部的自定義的委托,其具體的定義:
public delegate void Action()
可使用CancellationToken.Register方法完成對實例中方法的調(diào)用。如下有一個例子:
using System;using System.Threading;class CancelableObject{ public string id; public CancelableObject(string id) { this.id = id; } public void Cancel() { Console.WriteLine("Object {0} Cancel callback", id); // Perform object cancellation here. }}public class Example{ public static void Main() { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token; // User defined Class with its own method for cancellation var obj1 = new CancelableObject("1"); var obj2 = new CancelableObject("2"); var obj3 = new CancelableObject("3"); // Register the object's cancel method with the token's // cancellation request. token.Register(() => obj1.Cancel()); token.Register(() => obj2.Cancel()); token.Register(() => obj3.Cancel()); // Request cancellation on the token. cts.Cancel(); // Call Dispose when we're done with the CancellationTokenSource. cts.Dispose(); }}// The example displays the following output:// Object 3 Cancel callback// Object 2 Cancel callback// Object 1 Cancel callback
取消操作的監(jiān)聽與響應(yīng)方式
在一般情況下,在方法內(nèi)部使用使用Token.IsCancellationRequested屬性判斷其值,然后根據(jù)其值進行后續(xù)操作。這種模式可適應(yīng)大部分的情況。但是有些情況需要額外的處理方式。
特別是當(dāng)用戶在使用一些外部的library代碼時,上面提到的方式可能效果不好,更好的方法就是調(diào)用Token的方法 ThrowIfCancellationRequested(),讓它拋出異常OperationCanceledException,外部的Library截住異常,然后通過判斷異常的Token的相關(guān)屬性值,再進行相應(yīng)的處理。
ThrowIfCancellationRequested()的方法相當(dāng)于:
if (token.IsCancellationRequested) throw new OperationCanceledException(token);
因此在使用本方法時,通常的用法是(假設(shè)自己正在寫的代碼會被編譯為Library,供其他人調(diào)用,則自己寫的代碼應(yīng)該是這樣的):
if(!token.IsCancellationRequested){ //這兒正常的操作, //未被取消時,正常的代碼和邏輯操作實現(xiàn)}else{ //代表用戶進行了取消操作 //可以進行一些日志記錄 //注銷正在使用的資源 //然后就需要調(diào)用方法 token.ThrowIfCancellationRequested();}
當(dāng)別人使用Library時,需要在catch塊中監(jiān)聽OperationCanceledException異常,代碼如下:
try{ //調(diào)用Library的方法 library.doSomethingMethod();}catch(OperationCanceledException e1){ //捕獲這個異常,代表是用戶正常取消本操作,因此在這兒需要處理釋放資源之類的事情 xxx.dispose();}catch(exception e2){ //其他異常的具體處理方法}
以上是處理或?qū)懝﹦e人使用的Library或DLL時應(yīng)該遵循的方法。
在方法內(nèi)部進行處理相關(guān)流程時,對于監(jiān)聽用戶是否進行了取消操作,有如下的幾種方式:
1.輪詢式監(jiān)聽(Listening by Polling)
這種方法是最常用的,也是上面提到的,樣例如下:
static void NestedLoops(Rectangle rect, CancellationToken token){ for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++) { for (int y = 0; y < rect.rows; y++) { // Simulating work. Thread.SpinWait(5000); Console.Write("{0},{1} ", x, y); } // Assume that we know that the inner loop is very fast. // Therefore, checking once per row is sufficient. //就是下面的這句,通過for循環(huán)內(nèi)部的輪詢,去判斷IsCancellationRequested屬性值,從而去決定做其他的事情 if (token.IsCancellationRequested) { // Cleanup or undo here if necessary... Console.WriteLine("\r\nCancelling after row {0}.", x); Console.WriteLine("Press any key to exit."); // then... break; // ...or, if using Task: //若使用Task時,調(diào)用ThrowIfCancellationRequested方法,使其拋出異常 // token.ThrowIfCancellationRequested(); } }}
2.通過回調(diào)方法處理取消操作(Listening by Registering a Callback)
在比較復(fù)雜的情況下,可以使用register方法,注冊或登記取消回調(diào)方法。如下所示:
using System;using System.Net;using System.Threading;using System.Threading.Tasks;class CancelWithCallback{ static void Main() { var cts = new CancellationTokenSource(); var token = cts.Token; // Start cancelable task. // 這兒使用了一個Task,Task的使用和具體內(nèi)容可參見多線程(五) Task t = Task.Run( () => { WebClient wc = new WebClient(); // Create an event handler to receive the result. wc.DownloadStringCompleted += (obj, e) => { // Check status of WebClient, not external token. if (!e.Cancelled) { Console.WriteLine("The download has completed:\n"); Console.WriteLine(e.Result + "\n\nPress any key."); } else { Console.WriteLine("The download was canceled."); } }; // Do not initiate download if the external token has already been canceled. // 當(dāng)沒有收到取消消息時,則進行相關(guān)的下載。 // 并且在初始化時,進行了回調(diào)方法的登記,因此,當(dāng)token收到取消的方法時,則調(diào)用wc.CancelAsync() if (!token.IsCancellationRequested) { // Register the callback to a method that can unblock. using (CancellationTokenRegistration ctr = token.Register(() => wc.CancelAsync())) { Console.WriteLine("Starting request\n"); wc.DownloadStringAsync(new Uri("http://www.contoso.com")); } } }, token); Console.WriteLine("Press 'c' to cancel.\n"); char ch = Console.ReadKey().KeyChar; Console.WriteLine(); if (ch == 'c') cts.Cancel(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); cts.Dispose(); }}
在使用register方法時,有幾個注意事項:
1、callback方法盡量要快!不要阻礙線程!因此Cancel方法要等到callback方法結(jié)束后才返回
2、callback方法要盡量不要再使用多線程。
3.多對象關(guān)聯(lián)
可通過CancellationTokenSource的CreateLinkedTokenSource方法鏈接多個對象,從而形成一個新的CancellationTokenSource對象
鏈接中的任何一個對象使用了cancel方法,這個新的“鏈?zhǔn)健睂ο笠矔蝗∠?。如下?/p>
var cts1=new CancellationTokenSource();cts1.register(()=>Console.writeline("cts1被取消"));var cts2=new CancellationTokenSource();cts2.register(()=>Console.writeline("cts2被取消"));var linkcts=CancellationTokenSource.CreateLinkedTokenSource(cts1,cts2);linkcts.register(()=>Console.writeline("LinkCts被取消"));cts2.cancel();//其輸出結(jié)果如下://LinkCts被取消//cts2被取消
寫在本節(jié)學(xué)習(xí)最后
1、若自己的程序需要封裝為library,供其他人調(diào)用,則需要做好兩點:1、方法需要接受一個token作為參數(shù);2、需要較好的處理OperationCanceledException異常。
2、本節(jié)學(xué)習(xí)主要是結(jié)合:《CLR via C#》、MSDN的官網(wǎng)具體的網(wǎng)址在這兒, 以及網(wǎng)友的相關(guān)的文章。