《CLR via C#》讀書筆記-.NET多線程(四)

協作式取消
協作式取消其英文爲: cooperative cancellation model。在26.4節中只是很簡單的介紹了通過CancellationTokenSource來終結一個異步操作或長時間執行的同步操作。沒有具體的分析和說明爲什麼要這樣用。因爲終結一個異步操作的方法有很多,可以使用最簡單的truefalse變量結束異步操作。因此本次詳細整理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的官網具體的網址在這兒, 以及網友的相關的文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章