C#基础 - Cancellation
前言
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.NetworkStream
或System.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.Run
的cancellationToken
只是用于取消将委托封装成Task并添加到线程池的操作,一旦委托开始执行(几乎立即发生)cancellationToken
就没有任何作用了。只有在线程池严重饥饿的情况下,这个cancellationToken
参数才可能起作用。
如果真的想要用cancellationToken
来取消委托,至少应该这样写:
async Task 用户方法Async(CancellationToken cancellationToken)
{
var test = await Task.Run(() =>
{
cancellationToken.ThrowIfCancellationRequested(); //具体用法要根据用户逻辑来
//委托的用户逻辑
});
}
2,Cancellation的请求
2.1 引出CancellationTokenSource
为了引出CancellationTokenSource
,先讲讲CancellationToken
的创建。一般有3种方式:
- 由正在使用的框架或库提供。比如ASP.NET可以提供一个表示客户端断开连接的
CancellationToken
。 - 由用户使用
CancellationToken
构造函数或者CancellationToken.None
创建,这样创建出来的CancellationToken
在创建之初就处于未取消或已取消状态,并将一直保持此状态无法改变,在少数情况下会用到。 - 由用户创建的
CancellationTokenSource
获取,这是最通用的做法。每一个CancellationTokenSource
都有自己的CancellationToken
,此CancellationToken
只是一个小结构,它引用了其对应的CancellationTokenSource
。CancellationTokenSource
的作用是发出取消请求,被发出请求的代码持有。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 不响应
有些人喜欢用CancellationToken
的IsCancellationRequested
属性来判断轮询的结束,虽然达到了停止代码的目的,但这样会造成无法判断代码是正常结束还是响应取消停止的,因此不推荐这么写。
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
出结果。而新Task
与cancellationToken
进行了绑定,就可以实现新Task
的取消。
可以取消不可取消的异步操作吗?不行。
可以不等待不可取消的异步操作吗?可以,但是干这事得小心。
C#基础 - Cancellation的更多相关文章
- C#中的多线程 - 同步基础
原文:http://www.albahari.com/threading/part2.aspx 文章来源:http://blog.gkarch.com/threading/part2.html 1同步 ...
- D10——C语言基础学PYTHON
C语言基础学习PYTHON——基础学习D10 20180906内容纲要: 1.协程 (1)yield (2)greenlet (3)gevent (4)gevent实现单线程下socket多并发 2. ...
- D09——C语言基础学PYTHON
C语言基础学习PYTHON——基础学习D09 20180903内容纲要: 线程.进程 1.paramiko 2.线程.进程初识 3.多线程 (1)线程的调用方式 (2)join (3)线程锁.递归锁. ...
- vue基础知识之vue-resource/axios
Vue基础知识之vue-resource和axios(三) vue-resource Vue.js是数据驱动的,这使得我们并不需要直接操作DOM,如果我们不需要使用jQuery的DOM选择器,就没 ...
- C#中的多线程 - 同步基础 z
原文:http://www.albahari.com/threading/part2.aspx 专题:C#中的多线程 1同步概要Permalink 在第 1 部分:基础知识中,我们描述了如何在线程上启 ...
- Vue基础知识之vue-resource和axios
Vue基础知识之vue-resource和axios 原文链接:http://www.cnblogs.com/Juphy/p/7073027.html vue-resource Vue.js是数据驱 ...
- 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 ...
- linux 线程基础
线程基础函数 查看进程中有多少个线程,查看线程的LWP ps -Lf 进程ID(pid) 执行结果:LWP列 y:~$ ps -Lf 1887 UID PID PPID LWP C NLWP STIM ...
- Java基础】并发 - 多线程
Java基础]并发 - 多线程 分类: Java2014-05-03 23:56 275人阅读 评论(0) 收藏 举报 Java 目录(?)[+] 介绍 Java多线程 多线程任务执行 大多数 ...
- 论文翻译:2020_Acoustic Echo Cancellation Challenge Datasets And Testingframework
论文地址:ICASSP 2021声学回声消除挑战:数据集和测试框架 代码地址:https://github.com/microsoft/DNS-Challenge 主页:https://aec-cha ...
随机推荐
- PaddleNLP UIE -- 药品说明书信息抽取(名称、规格、用法、用量)
目录 创建项目 环境配置 上传代码 训练定制 代码结构 数据标注 准备语料库 数据标注 导出数据 数据转换 doccano Label Studio 模型微调 模型评估 定制模型--预测 效果 Pad ...
- MongoDB安装、基础操作和聚合实例详解
虽然MongoDB这些年很流行,但笔者之前没研究过,现在有需求研究这类NoSQL的数据库,是为了验证其是否可被替换. MongoDB是很轻量的文档数据库,简单测试也懒得专门准备虚拟机环境了,直接在ma ...
- 第三节 JMeter安装及配置
1.官网地址下载 (1)JDK:https://www.oracle.com/cn/java/technologies/downloads/,下载1.8版本以上的,最好下载最新版本(本次下载本次下载了 ...
- [rCore学习笔记 07]移除标准库依赖
改造Rust hello world 移除println!宏 rustc添加对裸机的支持 rustup target add riscv64gc-unknown-none-elf detail rus ...
- JAVA私有构造函数---java笔记
在Java中,构造函数是一种特殊的方法,它用于初始化新创建的对象.当我们创建一个类的实例时,构造函数会自动被调用. 构造函数可以有不同的访问修饰符,如public.protected.default( ...
- Prometheus 基于Python Django实现Prometheus Exporter
基于Python Django实现Prometheus Exporter 需求描述 运行监控需求,需要采集Nginx 每个URL请求的相关信息,涉及两个指标:一分钟内平均响应时间,调用次数,并且为每个 ...
- Django 多数据库配置与使用总结
Django 多数据库配置与使用总结 By:授客 QQ:103355122 #实践环境 Win 10 Python 3.5.4 Django-2.0.13.tar.gz 官方下载地址: https:/ ...
- 界面自动化测试录制工具,让python selenium自动化测试脚本开发更加方便
自动化测试中,QTP和selenium IDE都支持浏览器录制与回放功能,简单的来说就像一个记录操作步骤的机器人,可以按照记录的步骤重新执行一遍,这就是脚本录制.个人觉得传统录制工具有些弊端,加上要定 ...
- DNS在架构中的使用
1 介绍 DNS(Domain Name System,域名系统)是一种服务,它是域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP地址数串. ...
- 【Java】java.util.ConcurrentModificationException
异常提示信息: java.util.ConcurrentModificationException at java.util.LinkedHashMap$LinkedHashIterator.next ...