2019年8月27日 星期二

使用 Task.Run 中使用 CancellationTokenSource 與 CancellationToken 的注意事項

使用 Task.Run 中使用 CancellationTokenSource 與 CancellationToken 的注意事項

CancellationTokenSource Class 所建立的物件,可以取得一個 CancellationTokenSource.Token ,用於 向 CancellationToken 發出訊號,表示應該將它取消,也就是說,當要建立一個非同步應用工作的時候,將會在非同步工作內使用輪詢的方式來檢查,是否有發送出一個 取消執行 的訊號出來,一旦發現到這個訊號,對於非同步工作的程式碼可以選擇:(1) 正常結束與離開這個非同步處理作業 或者是 (2) 使用 CancellationToken.ThrowIfCancellationRequested 拋出一個 OperationCanceledException 例外異常出來,異常終止這個非同步處理作業;當然,不論使用哪種方式,當要終止非同步作業的時候,記得要將已經取得的資源歸還給系統與維持整個處理程序的資料一致性,避免整個處理程序處於不穩定的狀態。

了解更多關於 [Task Class] 的使用方式


了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式



在這篇文章中將會使用 Task.Run 來建立一個非同步工作 (如下面程式碼所列出),不過, Task.Run 方法,有這個多載,需要傳入一個委派方法與一個 CancellationToken 物件;如同前面的說明,在這個委派方法內,需要隨時檢查是否已經發出 取消訊號,這樣可以正常結束委派方法的執行,此時,在委派方法內需要能夠得到 CancellationToken 物件,對於這個 public static System.Threading.Tasks.Task Run (Action action, System.Threading.CancellationToken cancellationToken); 多載方法所傳入的 CancellationToken 物件,在非同步工作的委派方法內,是無法讀取到的,那麼,為什麼已經有在委派方法內檢查了取消權杖的訊號發送狀態,還需要有這樣的多載方法,再次傳入一個取消權杖物件到 Task.Run 方法內呢?
在這篇文章所提到的專案原始碼,可以從 GitHub 下載
C Sharp / C#
CancellationTokenSource cancellationToken = new CancellationTokenSource();
CancellationToken token = cancellationToken.Token;

// 狀況1 : 在非同步工作啟動後一秒種,發出取消執行訊號
cancellationToken.Cancel();

var MyTask = Task.Run(() =>
{
    Console.WriteLine("正在啟動非同步工作");
    Thread.Sleep(5000);
    if (token.IsCancellationRequested)
    {
        Console.WriteLine("非同步工作已經取消了");
    }
    Console.WriteLine("非同步工作結束了");
}, token);

// 狀況2 : 在非同步工作啟動後一秒種,發出取消執行訊號
//Thread.Sleep(1000);
// 狀況3 : 在非同步工作啟動後一秒種,發出取消執行訊號
//cancellationToken.Cancel();

Console.WriteLine("按下任一按鍵,檢查工作狀態");
Console.ReadKey();

Console.WriteLine($"工作狀態 {MyTask.Status}");

Console.WriteLine("Press any key for continuing...");
Console.ReadKey();

情境一:在啟動非同步工作前,就已經發出取消訊號

  • 請將狀況1 底下的 cancellationToken.Cancel(); 解除註解
  • 請將狀況2, 狀況3 後的程式碼註解起來,這包括了 Thread.Sleep(1000); 與 cancellationToken.Cancel();
  • 現在,請執行這個專案,底下是執行結果輸出內容
按下任一按鍵,檢查工作狀態
 工作狀態 Canceled
Press any key for continuing...
從執行結果可以看出,因為在非同步工作建立之前,就已經執行 cancellationToken.Cancel() 方法,發出了取消訊號,而在 Task.Run 的方法引數也有傳遞 CancellationToken 進去,因此,這個非同步工作所指定的委派方法尚未執行前,就已經取消了此非同步工作。
那麼,為什麼要有這樣的設計呢?這是因為要啟動與執行一個非同步工作是相當耗費計算機執行成本的,所以,若在執行非同步工作前,就已經發出取消訊號,最好的做法就是不要去執行非同步工作指定的委派方法,就直接取消非同步工作的執行,要不然,就需要開始執行非同步委派方法,等到該委派方法內才能夠檢查是否已經發出取消訊號了。

情境二:Task.Run 不要傳送 CancellationToken

  • 維持上述的程式碼,請將 Task.Run 方法內的第二個引數 , token,暫時移除
  • 請再度執行該專案
按下任一按鍵,檢查工作狀態
正在啟動非同步工作
 工作狀態 Running
Press any key for continuing...
非同步工作已經取消了
非同步工作結束了
從上述執行結果內容可以看出,雖然一開就已經送出取消訊號,還是會進入到非同步委派方法內來執行,所以會看到 正在啟動非同步工作 字串顯示在螢幕上,緊接著休息五秒鐘,才會檢查是否發送出取消訊號。這與狀況一的執行結果絕然不同,因此,若取消權杖在非同步工作啟動與執行前,就已經發送出訊號之後,最好是在 Task.Run 方法內,將取消權杖傳送進去。

情境三:執行 Task.Run 之後,立即送出權杖取消訊號

  • 請將狀態2 底下的 Thread.Sleep(1000); 程式碼註解
  • 請再度將 Task.Run 方法內,加入第二個引數 , token
  • 請將狀態3 後的程式碼解除註解,也就是 cancellationToken.Cancel();
  • 請再度執行該專案
按下任一按鍵,檢查工作狀態
 工作狀態 Canceled
Press any key for continuing...
在這裡,發送取消訊號,也就是 cancellationToken.Cancel(); 敘述是在 Task.Run 之後才執行,可是,不要以為就會直接進入到非同步工作的委派方法內,從上面的執行結果內容,可以看出,在 Task.Run 這個方法尚未啟動與執行非同步委派方法前,就已經偵測到權杖取消訊號了,因此,直接取消了此非同步作業。
會有這樣的結果,那是因為要啟動與執行非同步工作,需要一些時間,而 cancellationToken.Cancel(); 敘述緊接著 Task.Run 之後就直接執行,所以,這個敘述會先執行,之後非同步工作的運作環境準備好之後,需要開始執行時,就會檢查取消權杖訊號是否已經發送出,此時,就會看到這樣的執行結果輸出內容了。
+

情境四:執行 Task.Run 之後,等候 1 秒鐘,送出權杖取消訊號

  • 請將狀態2 底下的 Thread.Sleep(1000); 解除註解
  • 請將狀態3 底下的 cancellationToken.Cancel(); 解除註解
  • 請再度執行該專案
正在啟動非同步工作
按下任一按鍵,檢查工作狀態
非同步工作已經取消了
非同步工作結束了
 工作狀態 RanToCompletion
Press any key for continuing...
現在,非同步工作已經正常執行起來,接著才會發出取消權杖訊號,所以,當非同步工作委派方法執行之後的五秒鐘,在檢查取消權杖狀態的時候,就會終止非同步工作的作業

了解更多關於 [Task Class] 的使用方式


了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式






沒有留言:

張貼留言