首页 > 编程语言 >C#基础 - Cancellation

C#基础 - Cancellation

时间:2023-06-27 12:47:05浏览次数:56  
标签:cancellationToken await CancellationToken C# cts 基础 CancellationTokenSource 取消 C

目录

前言

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里的方法)。

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,
如果你想使自己的代码变成可取消的, 轮询是比较常用的方法。

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);  //同步代码
    }
}

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的取消。
可以取消不可取消的异步操作吗?不行。
可以不等待不可取消的异步操作吗?可以,但是干这事得小心。

标签:cancellationToken,await,CancellationToken,C#,cts,基础,CancellationTokenSource,取消,C
From: https://www.cnblogs.com/tossorrow/p/17505743.html

相关文章

  • Salesforce流程自动化Flow_Pause功能揭秘!
    通过自动化,帮助团队提升效率,将员工从那些重复、枯燥、耗时的工作中解放出来,转而从事更具创造性、更有价值的工作,是很多企业数字化转型朴素而又迫切的需求,也是世界No.1CRM——Salesforce的一大领先优势。FlowBuilder作为Salesforce自动化领域的新秀,逐渐处于重要地位,使用者需要......
  • dedecms 复制word里面带图文的文章,图片可以直接显示
    ​自动导入Word图片,或者粘贴Word内容时自动上传所有的图片,并且最终保留Word样式,这应该是Web编辑器里面最基本的一个需求功能了。一般情况下我们将Word内容粘贴到Web编辑器(富文本编辑器)中时,编辑器都无法自动上传图片。需要用户手动一张张上传Word图片。如果只有一张图片还能够接受,......
  • Bruce Eckel教你如何爬出 Gradle 的“坑”?
    王前明译《OnJava中文版》全书代码都是基于Gradle来构建的,很多书友表示对新手不够友好。刚开始接触Gradle时,Bruce本人也有类似的感受。不过,他最后选择用自己的方式克服这种“陌生感”,也对Gradle有了更深入的认识。本篇文章主要分享Bruce在学习Gradle时总结的一些方法论以及需要规......
  • C++太难学,怎么破?这本书给你指点迷津!
    2021年在线学习平台Springboard选出了最难学的编程语言TOP5,C++排在其中之一。C++难学的理由很多,比如它语法复杂,语法特性多,编程范式灵活,标准库内容过于基础,还要具备C语言基础等等。提起C++,它是由C语言大幅扩展而成,且用途非常广泛,例如用于Windows等操作系统、文字处理和......
  • CF1843E 二分+前缀和
    题意:给定一个长度为n且均为0的数组,q次单点修改(从0改为1),以及m个基于该数组的区间。规定好区间为:区间内1的个数严格大于0的个数。上述m个区间若存在一个好区间则为合法,问按顺序进行q次单点修改过程中最早出现合法的单次修改编号,若无则输出-1。马后炮思考:对于m个区间,其实际关系......
  • mac 使用 brew安装包报错 fatal: not in a git directory,Error: Command failed with
    在mac下使用brew安装包的时候,最后一行会报错:fatal:notinagitdirectoryError:Commandfailedwithexit128:git导致包安装不成功,解决办法:brew-v 绿色框就是提示你需要做的,输入gitconfig--global--addsafe.directory/opt/homebrew/Library/Taps/homeb......
  • mac 下使用 brew 安装包报错 error: Cannot install under Rosetta 2 in ARM default
    mac下使用brew安装包报错error:CannotinstallunderRosetta2inARMdefaultprefix(/opt/homebrew)!TorerununderARMuse:arch-arm64brewinstall...Toinstallunderx86_64,installHomebrewinto/usr/local. 解决:arch-arm64brewinstallxxx ......
  • 工业镜头基础知识
    工业镜头接口物镜的接口尺寸是有国际标准的,共有三种接口型式,即F型、C型、CS型。F型接口是通用型接口,一般适用于焦距大于25mm的镜头;而当物镜的焦距约小于25mm时,因物镜的尺寸不大,便采用C型或CS型接口。C接口:镜头与摄像机接触面至镜头焦平面距离为17.5mmCS接口:此距离为12.5mmC型......
  • system halt during installation with NV graphics card.
    Icheck,itseemsitisstuckat"GETubiquity/install_oem".Canyoucheck/var/cache/debconf/config.dat,iftheubiquity/install_oemvalueisTrue.itisin/usr/share/ubiquity/simple-pluginsscript,itsetthedbtotrueandgetitdirectlyin......
  • VINS-Mono: A Robust and Versatile Monocular Visual-Inertial State Estimator-翻译
    摘要:本文介绍了一种单目视觉惯性系统(VINS),用于在各种环境中进行状态估计。单目相机和低成本惯性测量单元(IMU)构成了六自由度状态估计的最小传感器套件。我们的算法通过有界滑动窗口迭代地优化视觉和惯性测量,以实现精确的状态估计。视觉结构是通过滑动窗口中的关键帧来维护的,而惯性......