使用 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
- CancellationToken
接下來我們就來瞭解這三種作法的程式碼是如何寫出來的呢?不過,不管是哪種做法,在我們呼叫非同步工作端,我們會進行執行這樣的程式碼 (如下所示):
- 建立一個 CancellationTokenSource 類別物件
- 這裡在呼叫建構式的時候,有指定 10 秒鐘的時間,代表當這個非同步工作超過了 10 秒鐘沒有執行完成的話,也會自動產生取消非同步工作執行請求。
- 建立一個背景執行緒,接受使用者輸入任一鍵盤按鍵,並且執行 CancellationTokenSource.Cancel() 方法,請求取消非同步工作的執行
- 接著,我們就會等候 await 非同步工作,並且將 CancellationTokenSource.Token 物件傳送到這個非同步工作內
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 例外異常,我們需要小心處理這個例外異常即可。
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 例外異常,我們需要小心處理這個例外異常即可。
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 例外異常,我們需要小心處理這個例外異常即可。
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;
}
了解更多關於 [CancellationTokenSource Class] 的使用方式
了解更多關於 [CancellationToken Struct] 的使用方式
關於 Xamarin 在台灣的學習技術資源
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程