前言

Cancellation即取消,常用于停止代码的执行。

原文是Stephen Cleary的博客 https://blog.stephencleary.com/2022/02/cancellation-1-overview.html

以及Stephen Toub的博客 https://devblogs.microsoft.com/pfxteam/how-do-i-cancel-non-cancelable-async-operations

1,概览

1.1 Cancellation是合作性的

取消的过程是一部分代码请求取消,另一部分代码响应该请求。请求代码只是礼貌地发出请求给另一部分代码它希望它停止,但实际上接收方代码可能会响应请求立刻停止,也可能忽略请求继续执行。这就是合作性的意思,通知方和接收方是合作关系,不是命令关系。

1.2 CancellationToken及其典型用法

CancellationToken是取消请求的接收方,后面会讲CancellationToken的创建和取消,目前只讲CancellationToken的典型用法。90%的情况下,我们都是在用户方法中添加CancellationToken参数,然后将其传递给调用的低级API(比如System.Net.Sockets.NetworkStreamSystem.IO.FileStream里的方法),这些低级API内部实现了CancellationToken的响应逻辑。

async Task 用户方法Async(int data, CancellationToken cancellationToken)
{
var 中间变量 = await 低级方法1号Async(data, cancellationToken);
await 低级方法2号Async(中间变量, cancellationToken);
}

CancellationToken被取消的方式很多:比如被用户点击按钮,客户端断开连接。我们一般不关心它是如何被取消的,只关心它有没有被取消。另外注意CancellationToken能且只能被取消一次,一旦取消则一直保持取消状态。

1.3 CancellationToken的响应

接收方响应取消请求时应该代码抛出OperationCanceledException异常。比如1.2中的代码在执行时,如果取消cancellationToken,那么低级方法1号Async低级方法2号Async会抛出OperationCanceledException异常,异常也会从传递到用户方法Async,响应取消时抛出异常是标准做法。

有些人在取消cancellationToken喜欢利用IsCancellationRequested属性来停止接收方代码,而不抛出OperationCanceledException异常,这种做法是不推荐的,因为这样难以推断代码是正常执行完成停止的还是响应取消请求停止的。

1.4 一个容易搞错的点

不应该将cancellationToken传递给Task.Run的参数。像下面这样:

async Task 用户方法Async(CancellationToken cancellationToken)
{
//坏代码!
var test = await Task.Run(() =>
{
//委托的用户逻辑
}, cancellationToken);
}

很多人以为cancellationToken取消时会取消委托,但事实并非如此。传递给Task.RuncancellationToken只是用于取消将委托封装成Task并添加到线程池的操作,一旦委托开始执行(几乎立即发生)cancellationToken就没有任何作用了。只有在线程池严重饥饿的情况下,这个cancellationToken参数才可能起作用。

如果真的想要用cancellationToken来取消委托,至少应该这样写:

async Task 用户方法Async(CancellationToken cancellationToken)
{
var test = await Task.Run(() =>
{
cancellationToken.ThrowIfCancellationRequested(); //具体用法要根据用户逻辑来
//委托的用户逻辑
});
}

2,Cancellation的请求

2.1 引出CancellationTokenSource

为了引出CancellationTokenSource,先讲讲CancellationToken的创建。一般有3种方式:

  1. 由正在使用的框架或库提供。比如ASP.NET可以提供一个表示客户端断开连接的CancellationToken
  2. 由用户使用CancellationToken构造函数或者CancellationToken.None创建,这样创建出来的CancellationToken在创建之初就处于未取消或已取消状态,并将一直保持此状态无法改变,在少数情况下会用到。
  3. 由用户创建的CancellationTokenSource获取,这是最通用的做法。每一个CancellationTokenSource都有自己的CancellationToken,此CancellationToken只是一个小结构,它引用了其对应的CancellationTokenSourceCancellationTokenSource的作用是发出取消请求,被发出请求的代码持有。CancellationToken的作用是响应取消请求,被可以被停止的代码持有。

2.2 CancellationTokenSource的使用

2.2.1 超时取消

超时取消是很常见的需求,效果是如果经过一段时间接收方代码还没执行完就停止执行。有两种方法创建超时取消:

async Task 用户方法TimeoutAsync()
{
//方法1:构造函数创建5分钟后超时的CancellationTokenSource
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)))
{
await 低级方法Async(cts.Token); //把CancellationToken传递给低级方法
}
}
async Task 用户方法TimeoutAsync()
{
using (CancellationTokenSource cts = new CancellationTokenSource())
{
//方法2:CancelAfter方法指定CancellationTokenSource5分钟后超时
cts.CancelAfter(TimeSpan.FromMinutes(5));
await 低级方法Async(cts.Token); //把CancellationToken传递给低级方法
}
}

2.2.2 手动取消

手动取消是更加通用的需求,比如用一个winform的按钮来请求取消:

private CancellationTokenSource _cts;

async void 开始按钮_Click(object sender, EventArgs e)
{
using (_cts = new CancellationTokenSource())
{
try
{
await 低级方法Async(_cts.Token);
}
catch (Exception ex)
{
//处理异常
}
}
}
async void 取消按钮_Click(object sender, EventArgs e)
{
_cts.Cancel(); //手动发出取消请求
}

为了避免用户操作UI时可能出现的报错,对按钮的可用性做一些限制

private CancellationTokenSource _cts;

public Form1()
{
InitializeComponent();
取消按钮.Enabled = false;
}
async void 开始按钮_Click(object sender, EventArgs e)
{
开始按钮.Enabled = false;
取消按钮.Enabled = true;
using (_cts = new CancellationTokenSource())
{
try
{
await 低级方法Async(_cts.Token);
}
catch (Exception ex)
{
//处理异常
}
finally
{
开始按钮.Enabled = true;
取消按钮.Enabled = false;
}
}
}
async void 取消按钮_Click(object sender, EventArgs e)
{
开始按钮.Enabled = true;
取消按钮.Enabled = false;
_cts.Cancel(); //手动发出取消请求 }

3,Cancellation的检测

前面说到取消是合作性的,有时请求取消的代码想确认操作到底是正常完成的还是响应取消停止的。

3.1 响应取消时检测

按照标准做法,持有CancellationToken的方法在响应取消时会抛出OperationCanceledException异常,改造一下第2节的开始按钮_Click方法:

async void 开始按钮_Click(object sender, EventArgs e)
{
using (_cts = new CancellationTokenSource())
{
try
{
await 低级方法Async(_cts.Token);
}
catch (OperationCanceledException) //一般不捕捉也不处理OperationCanceledException,除非你想知道取消到底有没有发生
{
//取消处理
}
catch (Exception ex)
{
//处理异常
}
}
}

3.2 TaskCanceledException

使用某些API时可能抛出TaskCanceledException而不是OperationCanceledException,实际上TaskCanceledException继承自 OperationCanceledException,因此不需要专门去捕获TaskCanceledException,捕获OperationCanceledException就行了。

3.3 OperationCanceledException.CancellationToken

OperationCanceledException有个CancellationToken属性,表示造成取消的token(不一定有值,API设置了才会有值,具体要看API的实现)。聪明的你可能会想可以利用它来与用户创建的CancellationToken做对比,来检测是用户取消了操作还是别的东西取消了操作。但实际上不推荐这么做,CancellationToken属性不一定是造成取消的根本原因(比如API里用到了LinkedTokenSource)。

async void 开始按钮_Click(object sender, EventArgs e)
{
//坏代码!
using (_cts = new CancellationTokenSource())
{
try
{
await 低级方法Async(_cts.Token);
}
catch (Exception ex) when (ex.CancellationToken == _cts.Token) //取消时,ex.CancellationToken可能不是_cts.Token
{
//处理异常
}
}
}

如果你确实想检测是不是用户取消了操作,推荐这么做:

async void 开始按钮_Click(object sender, EventArgs e)
{
//坏代码!
using (_cts = new CancellationTokenSource())
{
try
{
await 低级方法Async(_cts.Token);
}
catch (Exception ex) when (_cts.IsCancellationRequested)
{
//处理异常
}
}
}

4,Cancellation的响应

此处再强调一次,取消是合作性的,必须用CancellationToken响应取消请求才能实现取消。大部分情况下, 我们都是将CancellationToken传递给可取消的低级API, 低级API内部实现了CancellationToken的响应逻辑。

如果你想使自己的代码变成可取消的, 轮询是比较常用的方法。

4.1 如何响应

最通用的办法就是周期性地调用ThrowIfCancellationRequested:

void DoSomething(CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
Thread.Sleep(200); //同步代码
}
}

每次执行同步代码前都用ThrowIfCancelRequest检测一下取消请求,如果检测到将抛出OperationCanceledException异常。有一个需要考虑的问题是检测的频率,可以通过添加一个递增变量来调节。

void DoSomething(CancellationToken cancellationToken)
{
int i = 0;
while (true)
{
i++;
if (i > 10)
{
i = 0;
cancellationToken.ThrowIfCancellationRequested();
}
Thread.Sleep(200); //同步代码
}
}

4.2 不响应

有些人喜欢用CancellationTokenIsCancellationRequested属性来判断轮询的结束,虽然达到了停止代码的目的,但这样会造成无法判断代码是正常结束还是响应取消停止的,因此不推荐这么写。

void DoSomething(CancellationToken cancellationToken)
{
//坏代码!
while (!cancellationToken.IsCancellationRequested)
{
Thread.Sleep(200); //同步代码
}
}

4.3 有必要吗

这一小节是作者个人观点,在自己写的代码里真的有必要搞个CancellationToken吗?感觉定义一个bool变量就行了呀

bool done;
void DoSomething(CancellationToken cancellationToken)
{
while (!done)
{
Thread.Sleep(200); //同步代码
}
}

要停止时执行让done = true就好了,也容易判断代码是正常结束还是主动停止的。有人知道啥情况下应该在自己写的代码里使用CancellationToken吗?

5,取消不可取消的异步操作

这个问题本身不对,不可取消的操作当然是无法取消的。但既然有人提出来了,我们可以合理推测问题背后想表达的意思:让调用异步操作的代码不再等待异步操作完成,即不想等到await出结果。这与取消操作本身无关了,是完全是程序控制流的改变。

async void DoSomething(CancellationToken cancellationToken)
{
await 不可取消的异步方法Async(); //如何不等到await出结果就执行后续逻辑?
//后续逻辑
}

你可能会想是否有这么一个扩展方法WithCancellation,它可以检测cancellationToken并响应取消请求以停止等待。

async void DoSomething(CancellationToken cancellationToken)
{
await 不可取消的异步方法Async().WithCancellation(cancellationToken);
//后续逻辑
}

很遗憾官方并未提供这样的WithCancellation方法,因为这样做会导致代码不可靠,比如:

  • 如果异步操作最终完成并返回应该释放的对象,该怎么办?
  • 如果异步操作失败并出现被忽略的严重异常,该怎么办?
  • 如果异步操作仍在操作传递给它的引用参数同时后续逻辑也需要使用这个引用参数,该怎么办?

但如果你确实需要停止等待的功能,并能够妥善处理以上问题,几行代码就可以实现这个WithCancellation方法

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using(cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
{
throw new OperationCanceledException(cancellationToken);
}
return await task;
}
} async void DoSomething(CancellationToken cancellationToken)
{
try
{
await 不可取消的异步方法Async().WithCancellation(cancellationToken);
//后续逻辑
}
catch(OperationCanceledException)
{
//取消处理,但要仔细考虑避免代码变得不可靠
}
}

WithCancellation创建了一个新Task与原Task形成竞争,任一Task完成都会导致await出结果。而新TaskcancellationToken进行了绑定,就可以实现新Task的取消。

可以取消不可取消的异步操作吗?不行。

可以不等待不可取消的异步操作吗?可以,但是干这事得小心。

C#基础 - Cancellation的更多相关文章

  1. C#中的多线程 - 同步基础

    原文:http://www.albahari.com/threading/part2.aspx 文章来源:http://blog.gkarch.com/threading/part2.html 1同步 ...

  2. D10——C语言基础学PYTHON

    C语言基础学习PYTHON——基础学习D10 20180906内容纲要: 1.协程 (1)yield (2)greenlet (3)gevent (4)gevent实现单线程下socket多并发 2. ...

  3. D09——C语言基础学PYTHON

    C语言基础学习PYTHON——基础学习D09 20180903内容纲要: 线程.进程 1.paramiko 2.线程.进程初识 3.多线程 (1)线程的调用方式 (2)join (3)线程锁.递归锁. ...

  4. vue基础知识之vue-resource/axios

    Vue基础知识之vue-resource和axios(三)   vue-resource Vue.js是数据驱动的,这使得我们并不需要直接操作DOM,如果我们不需要使用jQuery的DOM选择器,就没 ...

  5. C#中的多线程 - 同步基础 z

    原文:http://www.albahari.com/threading/part2.aspx 专题:C#中的多线程 1同步概要Permalink 在第 1 部分:基础知识中,我们描述了如何在线程上启 ...

  6. Vue基础知识之vue-resource和axios

    Vue基础知识之vue-resource和axios  原文链接:http://www.cnblogs.com/Juphy/p/7073027.html vue-resource Vue.js是数据驱 ...

  7. Task C# 多线程和异步模型 TPL模型 【C#】43. TPL基础——Task初步 22 C# 第十八章 TPL 并行编程 TPL 和传统 .NET 异步编程一 Task.Delay() 和 Thread.Sleep() 区别

    Task C# 多线程和异步模型 TPL模型   Task,异步,多线程简单总结 1,如何把一个异步封装为Task异步 Task.Factory.FromAsync 对老的一些异步模型封装为Task ...

  8. linux 线程基础

    线程基础函数 查看进程中有多少个线程,查看线程的LWP ps -Lf 进程ID(pid) 执行结果:LWP列 y:~$ ps -Lf 1887 UID PID PPID LWP C NLWP STIM ...

  9. Java基础】并发 - 多线程

    Java基础]并发 - 多线程 分类: Java2014-05-03 23:56 275人阅读 评论(0) 收藏 举报 Java   目录(?)[+]   介绍 Java多线程 多线程任务执行 大多数 ...

  10. 论文翻译:2020_Acoustic Echo Cancellation Challenge Datasets And Testingframework

    论文地址:ICASSP 2021声学回声消除挑战:数据集和测试框架 代码地址:https://github.com/microsoft/DNS-Challenge 主页:https://aec-cha ...

随机推荐

  1. Android 7 修改启动动画和开机声音

    背景 在修改开机音量的时候,发现找不到对应的声音功能调用. 因此了解了一下安卓的开机声音是如何实现的. 安卓4~安卓7 都可以这么做. 参考: https://blog.csdn.net/chen82 ...

  2. 【算法】用c#实现计算方法中的经典降幂优化策略,减少计算复杂度

    对于给定的数组[x1,x2,x3,-,xn],计算幂的累积:x1^(x2^(x3^(-^xn))的最后一位(十进制)数字. 例如,对于数组[3,4,2],您的代码应该返回1,因为3^(4^2)=3^1 ...

  3. 解决方案 | Python中安装pix2tex latex ocr出现报错Cannot mix incompatible Qt library (6.6.2) with this library (6.7.2)

    一.问题 Python中安装pix2tex latex ocr出现报错Cannot mix incompatible Qt library (6.6.2) with this library (6.7 ...

  4. 10 pdf分享失败

    PC端分享pdf,复制粘贴pdf链接后跳转搜索首页

  5. WebGL压缩纹理实践

    0x01 本文将讲述压缩纹理在实际项目中的使用的案例.最近的一个项目是这样的:项目由于涉及到的建筑物特别多,大概有近40栋的建筑,而每一栋建筑物,又有10层楼,每层楼里面又有很多的设备.这就导致我们需 ...

  6. oeasy教您玩转vim - 87 - # 内容查找grep命令

    ​ 内容查找 grep 回忆 上次我们尝试了一下各种在vi中执行外部程序 可以排序 可以改大小写 还可以用管道 直接对于缓冲buffer文件进行操作 还是很方便的 其实还有一个外部命令很重要 根据内容 ...

  7. Swift开发基础08-高阶函数

    高阶函数是指接受其它函数作为参数,或者返回其它函数的函数.Swift 提供了许多内置的高阶函数,这些函数在处理集合类型数据(如数组.集合等)时尤其有用.常见的高阶函数包括 map.filter.red ...

  8. ASP.NET Core WebAPI 使用CreatedAtRoute通知消费者

    一.目的 我想告诉消费者我的api关于新创建的对象的位置 二.方法说明 public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult Cre ...

  9. Django model 层之Making Query总结

    Django model 层之Making Query总结 by:授客 QQ:1033553122 实践环境 Python版本:python-3.4.0.amd64 下载地址:https://www. ...

  10. 打开电脑属性 设置 windows

    组合键:win+R,输入sysdm.cpl,然后运行. 右键"此电脑",选择属性,然后点击高级系统设置. 组合键:win+Pause/Break. 在命令提示符中输入SystemP ...