2018年8月27日 星期一

使用 CancellationTokenSource 的 CancellationToken 於非同步工作中的三種取消請求作法

使用 CancellationTokenSource 的 CancellationToken 於非同步工作中的三種取消請求作法

當我們進行非同步或者多執行緒程式設計的時候,若想要通知非同步的工作或者執行緒終止執行工作或者停止執行,我們可以透過 CancellationTokenSource 類別來完成。在這個類別中有一個方法 CancellationTokenSource.Cancel() ,當我們執行這個方法的時候,就會送出取消執行的訊號到 CancellationToken 物件內。

了解更多關於 [使用 async 和 await 進行非
了解更多關於 [Task Class] 的使用方式
了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式

而當我們建立了 CancellationTokenSource 類別物件之後,最重要的是要把這個 CancellationTokenSource.Token 物件(其型別為 CancellationToken 類別),傳遞到非同步工作方法或者指定的執行緒內,這樣,在這些執行緒內我們就可以使用 CancellationToken 類別所提供的三種方法,提供終止這個非同步程式碼的執行,
這篇文章的範例程式碼,可以從 https://github.com/vulcanlee/CSharpNotes2018/tree/master/TaskCancel取得
其中,我們將會用到這兩個類別的物件,可以從底下文件查看到更多資訊
接下來我們就來瞭解這三種作法的程式碼是如何寫出來的呢?不過,不管是哪種做法,在我們呼叫非同步工作端,我們會進行執行這樣的程式碼 (如下所示):
  • 建立一個 CancellationTokenSource 類別物件
    • 這裡在呼叫建構式的時候,有指定 10 秒鐘的時間,代表當這個非同步工作超過了 10 秒鐘沒有執行完成的話,也會自動產生取消非同步工作執行請求。
  • 建立一個背景執行緒,接受使用者輸入任一鍵盤按鍵,並且執行 CancellationTokenSource.Cancel() 方法,請求取消非同步工作的執行
  • 接著,我們就會等候 await 非同步工作,並且將 CancellationTokenSource.Token 物件傳送到這個非同步工作內
C Sharp / C#
CancellationTokenSource ctsIsCancellationRequested = new CancellationTokenSource(TimeSpan.FromSeconds(10));
ThreadPool.QueueUserWorkItem((s) =>
{
    Console.WriteLine($"(IsCancellationRequested)請按下任一按鍵或者等候10秒鐘,該非同步工作就會取消");
    Console.ReadKey();
    ctsIsCancellationRequested.Cancel();
});
try
{
    await MethodIsCancellationRequestedAsync(ctsIsCancellationRequested.Token);
}
catch (OperationCanceledException oce)
{
    Console.WriteLine($"已經捕捉到使用者發出的工作取消例外異常 {oce.Message}");
}
現在,讓我們來看看這三中做法,其中,我們的非同步工作將會使用 TaskCompletionSource 類別來建立,並且使用 Task.Run 方法,建立一個執行緒,讓這個非同步工作在這個執行緒內來執行。每個執行緒內會執行 100 次的迴圈,每次都會休息 0.5 秒鐘。

使用 IsCancellationRequested 來處理工作取消請求

第一種作法將是採用輪詢 Polling 的方式,隨時檢查 CancellationToken.IsCancellationRequested 這個屬性是否為真,若這個屬性為真,則代表在其他的執行緒上,有程式碼呼叫了 CancellationTokenSource.Cancel() 方法,發出了需要取消非同步工作執行的請求。因此,我們需要經常性地去檢查這個 CancellationToken.IsCancellationRequested 屬性值為真,我們就需要結束這個非同步執行緒的委派方法執行,這樣就會結束 / 取消 非同步工作了。在這裡,我們僅需要 TaskCompletionSource.SetCanceled() 這個方法,告知這個 Task (非同步工作) 物件,該工作已經取消了,在呼叫等候端的程式碼,將會得到 OperationCanceledException 例外異常,我們需要小心處理這個例外異常即可。
C Sharp / C#
private static Task MethodIsCancellationRequestedAsync(CancellationToken token)
{
    TaskCompletionSource<DateTime> tcs = new TaskCompletionSource<DateTime>();
    Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(500);
            Console.Write(".");
            if (token.IsCancellationRequested == true)
            {
                Console.WriteLine($"使用者已經提出取消請求");
                tcs.SetCanceled();
                break;
            }
        }
    });
    return tcs.Task;
}

使用 ThrowIfCancellationRequested 來處理工作取消請求

第二種作法將是採用輪詢 Polling 的方式,不過,我們需要隨時執行 CancellationToken.ThrowIfCancellationRequested() 這個方法,一旦其他的執行緒上,有程式碼呼叫了 CancellationTokenSource.Cancel() 方法,並且我們也執行了 CancellationToken.ThrowIfCancellationRequested() 這個方法,那就會產生出一個 OperationCanceledException 例外異常,若每有發出取消請求,則執行 CancellationToken.ThrowIfCancellationRequested() 這個方法,是不會有任何動作產生的。因此,我們需要經常性地去執行 CancellationToken.ThrowIfCancellationRequested() 這個方法。不過,在這裡要特別注意,因為我們的非同步工作使用 TaskCompletionSource 來設計,因此,若要讓這個 Task 工作物件完成此次非同步執行(最後結果可能是 完成、失敗、取消),我們需要特別指定這個工作是使用甚麼方式來完成,因此,若是因為有取消請求,我們還需要執行 TaskCompletionSource.SetCanceled() 這個方法。
在我們剛剛的設計中,若先執行了 CancellationToken.ThrowIfCancellationRequested() 這個方法,並且已經有 CancellationTokenSource.Cancel() 方法執行了,這個非同步執行緒就會立即結束,但是我們還需要執行 TaskCompletionSource.SetCanceled() 方法,否則,您的程式將會無窮盡的持續等候這個工作結果。因此,我們將 CancellationToken.ThrowIfCancellationRequested() 這個方法 放在一個 try...catch 區段內,若產生了 任何例外異常(當然是要 OperationCanceledException 物件),我們就可以在 catch 區段內執行 TaskCompletionSource.SetCanceled() 這個方法,告知這個 Task (非同步工作) 物件,該工作已經取消了,在呼叫等候端的程式碼,將會得到 OperationCanceledException 例外異常,我們需要小心處理這個例外異常即可。
C Sharp / C#
private static Task MethodThrowIfCancellationRequestedAsync(CancellationToken token)
{
    TaskCompletionSource<DateTime> tcs = new TaskCompletionSource<DateTime>();
    Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(500);
            Console.Write(".");
            #region 不可行的模式
            //// 當收到 Cancel() 方法呼叫,底下的方法會造成例外異常,整個程式會被凍結
            //token.ThrowIfCancellationRequested();
            #endregion

            try
            {
                token.ThrowIfCancellationRequested();
            }
            catch
            {
                // 底下這行會造成在除錯模式下,該執行緒有例外異常中斷發生
                tcs.SetCanceled();
            }
        }
    });
    return tcs.Task;
}

使用 Register 事件 來處理工作取消請求

第三種作法將是採用 CancellationToken.Register 事件來處理,我們需要訂閱這個事件,並且指定一個委派方法,進行當接收到 CancellationTokenSource.Cancel() 方法有執行的通知。這裡需要特別的注意,當 CancellationToken.Register 事件之委派方法被執行的時候,這個時候將會在一個新的執行緒中來執行,也就是與執行非同步工作的執行緒是不同的,因此,我們無法簡單的在 CancellationToken.Register 事件的委派方法內,使用 Thread.CurrentThread.Abort() 這個方法,來讓非同步工作的執行緒中止執行,因為,您將會終止現在正在執行的執行緒,也就是 CancellationToken.Register 事件之委派方法。
因此,解決作法將是,當使用 Task.Run 產生一個非同步執行緒的值後,立即將這個執行緒物件儲存起來,當在 CancellationToken.Register 事件之委派方法被執行的時候,我們可以使用剛剛儲存起來的執行緒物件,呼叫該執行緒的 Abort() 方法,這樣,這個非同步工作執行緒就會終止執行了。在這裡,我們是在 CancellationToken.Register 事件之委派方法 啟用另外一個執行緒,來完成這項工作 ThreadPool.QueueUserWorkItem(x =>{thread.Abort();});
當然,還是不要忘記要執行 TaskCompletionSource.SetCanceled() 這個方法,讓這個 Task 工作物件完成此次非同步執行,其結果是被取消了,最後,在呼叫等候端的程式碼,將會得到 OperationCanceledException 例外異常,我們需要小心處理這個例外異常即可。
C Sharp / C#
private static Task MethodRegisterCallBackAsync(CancellationToken token)
{
    Thread thread;
    bool isCancle = false;
    TaskCompletionSource<DateTime> tcs = new TaskCompletionSource<DateTime>();
    Task.Run(() =>
    {
        thread = Thread.CurrentThread;
        Console.WriteLine($"非同步執行緒 ID = {Thread.CurrentThread.ManagedThreadId}");
        #region Register不正確的作法
        token.Register(() =>
        { 
            Console.WriteLine($"Register事件內 執行緒 ID = {Thread.CurrentThread.ManagedThreadId}");
            // 方法1: 設定要取消旗標為真,讓這個執行緒結束執行
            isCancle = true;

            #region 類另做法,啟動另外一個執行緒,終止非同步執行緒。因為當執行 SetCanceled() 方法後,就無法再執行其他敘述了
            Console.WriteLine($"準備針對非同步工作 執行緒 ID = {thread.ManagedThreadId} 下達終止請求");
            // 方法2: 啟動另外一個執行緒,下達執行緒終止執行的請求
            ThreadPool.QueueUserWorkItem(x =>
            {
                thread.Abort();
            });
            #endregion

            // 下達這個方法,也只是造成非同步工作完成,但原來執行緒上的工作,卻還是繼續在執行
            tcs.SetCanceled();
            // 下達該指令也是無效的,因為,在該事件內與非同步工作內的執行緒 是不同的
            //Thread.CurrentThread.Abort();
        }, true);
        #endregion
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(500);
            Console.Write(".");
            // 方法1: 在這裡判斷要讓這個執行緒結束執行
            //if (isCancle) break;
        }

    });
    return tcs.Task;
}

了解更多關於 [使用 async 和 await 進行非
了解更多關於 [Task Class] 的使用方式
了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程



沒有留言:

張貼留言