協作式取消
協作式取消其英文爲: cooperative cancellation model。在26.4節中只是很簡單的介紹了通過CancellationTokenSource來終結一個異步操作或長時間執行的同步操作。沒有具體的分析和說明爲什麼要這樣用。因爲終結一個異步操作的方法有很多,可以使用最簡單的true
和false
變量結束異步操作。因此本次詳細整理CLR的在線程取消的模式。本文參考了MSDN及其他網友的相關資料,具體的引用會在文章的尾端。
從.NET4開始,.NET Framework才爲異步或需長時間執行的同步操作提供了協作取消模式。通常使用的有兩個“東西“,一個是CancellationTokenSource,另一個是struct:CancellationToken。前者是取消請求的發起者,而後者是消息請求的監聽者。就像量子世界中的量子糾纏一樣,一個是根據現場的環境做出相應的響應,而另一個會立刻做出反應。CancellationTokenSource與CancellationToken就是這樣的一個狀態。
協作式取消的使用
協作式取消的使用步驟如下:
1、創建CancellationTokenSource實例
2、使用CancellationTokenSource實例的Token屬性,獲取CancellationToken,並將其傳至Task或線程的相關方法中
3、在task或thread中提供根據CancellationToken.IsCancellationRequested屬性值進行判定是否應該停止操作的機制
4、在程序中調用CancellationTokenSource實例的cancel方法
這兒有一篇文章,是使用CancellationTokenSource的具體例子。.Net 4.5中通過CancellationTokenSource實現對超時任務的取消
CancellationTokenSource
1、定義
CancellationTokenSource類的定義如下:
[ComVisibleAttribute(false)]
[HostProtectionAttribute(SecurityAction.LinkDemand, Synchronization = true,
ExternalThreading = true)]
public class CancellationTokenSource : IDisposable
因本類實現了IDisposable的方法,因此在用完時需調用其dispose方法,或者是使用using
2、CancellationTokenSource與CancellationToken的關係
兩者的關係如圖所示:
通過這張圖,可得出:
1、不同的操作使用相同的CancellationTokenSource實例,就可以達到一次調用取消多個操作的目的。
2、CancellationToken爲什麼會是struct,而不是類
3、其他說明
1、除了CancellationTokenSource與CancellationToken之外,還有一個OperationCanceledException異常類,這個overload的異常類接受Token作爲參數,因此在判斷具體異常時,可使用本類
4、代碼說明
代碼如下:
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...
以上方法使用的系統遺留方式,但是希望停止一個task時,參見如下:How to: Cancel a Task and Its Children
操作取消與對象取消(Operation Cancellation Versus Object Cancellation)
在協作式取消操作中,通常都是在方法中通過判斷Token的IsCancellationRequested屬性,然後根據這個屬性的值對操作(或方法)進行相應的處理。因此,常用的協作式取消模式就是Operation Cancellation。PS.Token的IsCancellationRequested只能被設置一次,即當該屬性被設置爲true時,其不可能再被設爲false,不能重複利用。另外,Token在被“用過”後,不能重複使用該對象。即,CancellationTokenSource對象只能使用一次,若希望重複使用,需要在每次使用時,創建新的對象。
除了操作取消之外,還有另外一種情況,我希望當CancellationTokenSource實例調用cancel方法時,調用某個實例中的某個方法。而這個方法內部沒有CancellationToken對象。這個時候可以使用CancellationTokenSource的Register方法。
方法的定義如下:
public CancellationTokenRegistration Register(Action callback)
其中Action是.NET內部的自定義的委託,其具體的定義:
public delegate void Action()
可使用CancellationToken.Register方法完成對實例中方法的調用。如下有一個例子:
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
取消操作的監聽與響應方式
在一般情況下,在方法內部使用使用Token.IsCancellationRequested屬性判斷其值,然後根據其值進行後續操作。這種模式可適應大部分的情況。但是有些情況需要額外的處理方式。
特別是當用戶在使用一些外部的library代碼時,上面提到的方式可能效果不好,更好的方法就是調用Token的方法 ThrowIfCancellationRequested(),讓它拋出異常OperationCanceledException,外部的Library截住異常,然後通過判斷異常的Token的相關屬性值,再進行相應的處理。
ThrowIfCancellationRequested()的方法相當於:
if (token.IsCancellationRequested)
throw new OperationCanceledException(token);
因此在使用本方法時,通常的用法是(假設自己正在寫的代碼會被編譯爲Library,供其他人調用,則自己寫的代碼應該是這樣的):
if(!token.IsCancellationRequested)
{
//這兒正常的操作,
//未被取消時,正常的代碼和邏輯操作實現
}else
{
//代表用戶進行了取消操作
//可以進行一些日誌記錄
//註銷正在使用的資源
//然後就需要調用方法
token.ThrowIfCancellationRequested();
}
當別人使用Library時,需要在catch塊中監聽OperationCanceledException異常,代碼如下:
try
{
//調用Library的方法
library.doSomethingMethod();
}
catch(OperationCanceledException e1)
{
//捕獲這個異常,代表是用戶正常取消本操作,因此在這兒需要處理釋放資源之類的事情
xxx.dispose();
}
catch(exception e2)
{
//其他異常的具體處理方法
}
以上是處理或寫供別人使用的Library或DLL時應該遵循的方法。
在方法內部進行處理相關流程時,對於監聽用戶是否進行了取消操作,有如下的幾種方式:
1.輪詢式監聽(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循環內部的輪詢,去判斷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時,調用ThrowIfCancellationRequested方法,使其拋出異常
// token.ThrowIfCancellationRequested();
}
}
}
2.通過回調方法處理取消操作(Listening by Registering a Callback)
在比較複雜的情況下,可以使用register方法,註冊或登記取消回調方法。如下所示:
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的使用和具體內容可參見多線程(五)
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.
// 當沒有收到取消消息時,則進行相關的下載。
// 並且在初始化時,進行了回調方法的登記,因此,當token收到取消的方法時,則調用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方法結束後才返回
2、callback方法要儘量不要再使用多線程。
3.多對象關聯
可通過CancellationTokenSource的CreateLinkedTokenSource方法鏈接多個對象,從而形成一個新的CancellationTokenSource對象
鏈接中的任何一個對象使用了cancel方法,這個新的“鏈式”對象也會被取消。如下:
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();
//其輸出結果如下:
//LinkCts被取消
//cts2被取消
寫在本節學習最後
1、若自己的程序需要封裝爲library,供其他人調用,則需要做好兩點:1、方法需要接受一個token作爲參數;2、需要較好的處理OperationCanceledException異常。
2、本節學習主要是結合:《CLR via C#》、MSDN的官網具體的網址在這兒, 以及網友的相關的文章。