背景
在.Net和C#中運行異步代碼相當簡單,因為我們有時候需要取消正在進行的異步操作,通過本文,可以掌握 通過CancellationToken取消任務(包括non-cancellable任務)。
Task 表示無返回值的異步操作, 泛型版本Task<TResult>表示有返回值的異步操作, 現在async/await 語法糖大大簡化了我們編寫異步程序的難度。

/// <summary> /// Compute a value for a long time. /// </summary> /// <returns>The value computed.</returns> /// <param name="loop">Number of iterations to do.</param> private static Task<decimal> LongRunningOperation(int loop) { // Start a task and return it return Task.Run(() => { decimal result = 0; // Loop for a defined number of iterations for (int i = 0; i < loop; i++) { // Do something that takes times like a Thread.Sleep in .NET Core 2. Thread.Sleep(10); result += i; } return result; }); } // 這里我們使用Thread.Sleep 模仿長時間運行的操作
簡單異步調用代碼:
public static async Task ExecuteTaskAsync() { Console.WriteLine(nameof(ExecuteTaskAsync)); Console.WriteLine("Result {0}", await LongRunningOperation(100)); Console.WriteLine("Press enter to continue"); Console.ReadLine(); }
因為一些原因我們會取消異步操作:
- 操作耗時較長,堵塞了其他正常請求;
- 不願意再等待執行結果了,手動取消
編寫可取消的異步操作代碼
其中關注
類CancellationTokenSource:給CancellationToken發出取消通知
結構體CancellationToken: 取消操作的通知。
CancellationToken結構體相當於打入在異步操作內部的楔子,隨時等候CancellationTokenSource 發出的取消通知。
定義異步方法時候設定 CancelletionToken參數
那么這個異步方法即是Cancellable 的異步方法
/// <summary> /// Compute a value for a long time. /// </summary> /// <returns>The value computed.</returns> /// <param name="loop">Number of iterations to do.</param> /// <param name="cancellationToken">The cancellation token.</param> private static Task<decimal> LongRunningCancellableOperation(int loop, CancellationToken cancellationToken) { Task<decimal> task = null; // Start a task and return it task = Task.Run(() => { decimal result = 0; // Loop for a defined number of iterations for (int i = 0; i < loop; i++) { // Check if a cancellation is requested, if yes, // throw a TaskCanceledException. if (cancellationToken.IsCancellationRequested) throw new TaskCanceledException(task); // Do something that takes times like a Thread.Sleep in .NET Core 2. Thread.Sleep(10); result += i; } return result; }); return task; }
發送取消通知
操縱以上CancellationToken狀態的對象是 CancellationTokenSource,這個對象是取消操作的命令發布者。
// 定義超時取消 public static async Task ExecuteTaskWithTimeoutAsync(TimeSpan timeSpan) { Console.WriteLine(nameof(ExecuteTaskWithTimeoutAsync)); using (var cancellationTokenSource = new CancellationTokenSource(timeSpan)) { try { var result = await LongRunningCancellableOperation(500, cancellationTokenSource.Token); Console.WriteLine("Result {0}", result); } catch (TaskCanceledException) { Console.WriteLine("Task was cancelled"); } } Console.WriteLine("Press enter to continue"); Console.ReadLine(); }
------------------------------------------------------------------------------------------------------------
手動取消操作
public static async Task ExecuteManuallyCancellableTaskAsync() { Console.WriteLine(nameof(ExecuteManuallyCancellableTaskAsync)); using (var cancellationTokenSource = new CancellationTokenSource()) { // Creating a task to listen to keyboard key press var keyBoardTask = Task.Run(() => { Console.WriteLine("Press enter to cancel"); Console.ReadKey(); // Cancel the task cancellationTokenSource.Cancel(); }); try { var longRunningTask = LongRunningCancellableOperation(500, cancellationTokenSource.Token); var result = await longRunningTask; Console.WriteLine("Result {0}", result); Console.WriteLine("Press enter to continue"); } catch (TaskCanceledException) { Console.WriteLine("Task was cancelled"); } await keyBoardTask; } }
// 以上是一個控制台程序,異步接收控制台輸入,發出取消命令。
附: 取消non-Cancellable任務 :
有時候,部分第三方異步操作代碼並不是可取消的,也就是以上長時間運行的異步操作LongRunningCancellableOperation(int loop, CancellationToken cancellationToken) 並不支持CancellationToken ,相當於不允許打入楔子。
這時我們怎樣取消 這樣的non-Cancellable 任務?
可考慮利用 Task.WhenAny( params tasks) 操作曲線取消:
- 利用TaskCompletionSource 注冊異步可取消任務
- 等待待non-cancellable 操作和以上建立的 異步取消操作
private static async Task<decimal> LongRunningOperationWithCancellationTokenAsync(int loop, CancellationToken cancellationToken) { // 定義一個任務完成的消息源,任務取消的動作 綁定到該任務完成的動作上 var taskCompletionSource = new TaskCompletionSource<decimal>(); cancellationToken.Register(() => { taskCompletionSource.TrySetCanceled(); }); var task = LongRunningOperation(loop);
var completedTask = await Task.WhenAny(task, taskCompletionSource.Task); return await completedTask; }
像上面代碼一樣執行取消命令 :
public static async Task CancelANonCancellableTaskAsync() { Console.WriteLine(nameof(CancelANonCancellableTaskAsync)); using (var cancellationTokenSource = new CancellationTokenSource()) { // Listening to key press to cancel var keyBoardTask = Task.Run(() => { Console.WriteLine("Press enter to cancel"); Console.ReadKey(); // Sending the cancellation message cancellationTokenSource.Cancel(); }); try { // Running the long running task var longRunningTask = LongRunningOperationWithCancellationTokenAsync(100, cancellationTokenSource.Token); var result = await longRunningTask; Console.WriteLine("Result {0}", result); Console.WriteLine("Press enter to continue"); } catch (TaskCanceledException) { Console.WriteLine("Task was cancelled"); } await keyBoardTask; } }
總結:
大多數情況下,我們不需要編寫自定義可取消任務,因為我們只需要使用現有API。但要知道它是如何在幕后工作總是好的。
+ https://johnthiriet.com/cancel-asynchronous-operation-in-csharp/
+ https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout