【异步编程】Part3:取消异步操作
背景
在.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
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/10572840.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化