前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Dotnet线程取消的深度进阶(一)

Dotnet线程取消的深度进阶(一)

作者头像
老王Plus
发布2022-04-13 15:05:01
3120
发布2022-04-13 15:05:01
举报
文章被收录于专栏:老王Plus

Dotnet 编程中,会玩取消,才算是真正会玩异步和多线程。

这个话题有点长,估计得分个几篇写。

取消的概念

通常我们最熟悉的,是一个方法的中止。中止是完全的。一个方法中止了,则这个方法不再往下执行,方法中前面已经完成的部分会被抛弃,并返回一个设定的结果。

取消则不同。

通常,取消是由其它代码发出的命令,也就是说,是由一些代码去请求取消,另一部分代码的响应取消。而且,实际发生的情况,是请求代码只是通知响应代码,希望它能停止执行;响应代码会按照自己设定的方式对取消请求做出响应,有可能立即停止任务,也有可能继续运行下去,直到一个可以停止的点,甚至可能完全忽略这个取消请求。

概念清楚了,怎么做?

取消令牌

既然是一方请求,另一方响应,那对于响应代码来说,重要的是能够知道并响应取消请求。

在 Dotnet 里,给出了一个东西,叫取消令牌 ( Cancellation Tokens )。这个令牌,就是请求取消的载体。

请求代码发起取消时,实际是发起了一个对「取消令牌」的取消操作,然后,响应代码将对这个被取消的令牌做出正确反应。

如果看到这儿有点混乱的话,看一下示例代码:

代码语言:javascript
复制
async Task SomethingAsync(int data, CancellationToken cancellationToken)
{
    var result = await FirstStepAsync(data, cancellationToken);
    await SecondStepAsync(intermediateValue, cancellationToken);
}

响应代码基本都是这个样子。这里面,CancellationToken 就是上面说的取消令牌。

CancellationToken 可以在任何地方被设置为取消:用户按下取消按钮,或客户端断开连接,超时,等等。重要的是,当它被设置为取消时,就表示响应代码需要处理取消了。

注意:一个 CancellationToken 只能被取消一次。一旦它被取消,就会永远保持取消状态。

带有取消令牌的方法定义

上面的示例,就是一个典型的带有取消令牌的方法定义。

按照微软的习惯,带有 CancellationToken 的方法有以下约定:

  • CancellationToken 通常是最后一个参数
  • 方法通常会提供一个重载,或默认参数值,以便调用者可以不提供取消令牌而直接调用

当然,这是一个非强制的约定。如果你不介意别人看着别扭,可以不管这个约定。

看几个例子:

代码语言:javascript
复制
Task SomethingAsync(int data) => SomethingAsync(data, CancellationToken.None);

async Task SomethingAsync(int data, CancellationToken cancellationToken)
{
    ...
}

async Task SomethingAsync(int data, CancellationToken cancellationToken = default)
{
    ...
}

在这里,CancellationToken 代表任何类型或任何原因的取消。

通过 CancellationToken 参数,方法声明了自己可以响应取消。而实际上,这只是个声明。代码中,CancellationToken 可能会被忽略。因此,有这个声明仅仅表示方法可能支持取消,而不是一定支持。

方法对取消的响应

上面说到了,响应代码可以响应取消,也可以不取消。

而即使响应代码真的去响应取消,通常也会有不同的情况。

通常来说,如果取消请求到达时,响应方法实际取消了一些工作,会抛出 OperationCanceledException 来通知调用程序;而如果取消被忽略,或者取消请求来的太晚而任务已经完成,那响应方法会正常返回,而且不抛出 OperationCanceledException 异常。这个在微软的基础类库(BCL)中,体现得很明显。

大多数情况下,异常会被逐层传出。再看一下上面的例子:

代码语言:javascript
复制
async Task SomethingAsync(int data, CancellationToken cancellationToken)
{
    var result = await FirstStepAsync(data, cancellationToken);
    await SecondStepAsync(intermediateValue, cancellationToken);
}

如果 FirstStepAsync 或 SecondStepAsync 抛出 OperationCanceledException,那这个异常也会从 SomethingAsync 中传出给调用者。

这里要强调一下:看过很多代码,在请求取消时会不抛出异常而直接返回。不要这样做。调用者不知道这个取消是被接受,还是被忽略,会出大问题的。

一个常见的错误用法

在代码 Review 时,见过好几次这样的情况:

代码语言:javascript
复制
async Task SomethingAsync(CancellationToken cancellationToken)
{
    var test = await Task.Run(() =>
    {
      ...
    }, cancellationToken);
    ...
}
// 注意,这个例子的写法是错的。

这个有必要专门拿出来说一下。

很多人把委托和 CancellationToken 传递给 Task,期望在令牌取消时取消委托。注意,这个理解是错的。

Task.Run 是对线程池的委托调度,是一个立即完成的瞬时动作。CancellationToken 在这儿的作用是取消调度这个动作,而这个动作是立即完成的,换句说说,一旦走到这一行,调度操作会立即完成,这个取消令牌也就没有用了,会被忽略。

所以,这种情况不需要用 CancellationToken,要写成下面的方式:

代码语言:javascript
复制
async Task SomethingAsync(CancellationToken cancellationToken)
{
    var test = await Task.Run(( cancellationToken ) =>
    {
      ...
    });
    ...
}

写成这样,才是正确的表达,表达委托本身需要响应令牌。

这是一个容易搞错的知识点,记一下。

第一部分就写这么多了。

最近停更了有接近两个月。家里老爸病了,需要照顾,这几天缓了一些,所以更新继续。

咱们这一行有多累,大家都清楚。所以提醒一下大家:

照顾好自己,也照顾好自己的亲人。

有健康,才是根本;

爸妈在,才是幸福。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 老王Plus 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 取消的概念
  • 取消令牌
  • 带有取消令牌的方法定义
  • 方法对取消的响应
  • 一个常见的错误用法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档