Dotnet线程取消的深度进阶

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

取消的概念

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

取消则不同。

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

概念清楚了,怎么做?

取消令牌

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

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

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

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

async Task SomethingAsync(int data, CancellationToken cancellationToken)
{
var result = await FirstStepAsync(data, cancellationToken);
await SecondStepAsync(intermediateValue, cancellationToken);
}

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

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

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

带有取消令牌的方法定义

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

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

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

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

看几个例子:

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)中,体现得很明显。

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

async Task SomethingAsync(int data, CancellationToken cancellationToken)
{
var result = await FirstStepAsync(data, cancellationToken);
await SecondStepAsync(intermediateValue, cancellationToken);
}

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

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

一个常见的错误用法

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

async Task SomethingAsync(CancellationToken cancellationToken)
{
var test = await Task.Run(() =>
{
...
}, cancellationToken);
...
}
// 注意,这个例子的写法是错的。

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

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

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

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

async Task SomethingAsync(CancellationToken cancellationToken)
{
var test = await Task.Run(( cancellationToken ) =>
{
...
});
...
}

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

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

责任编辑:武晓燕 来源: 老王Plus
相关推荐

2021-12-29 07:44:50

Dotnet 代码系统

2021-01-20 08:16:06

异步Dotnet Core多路径

2021-10-27 09:59:35

存储

2024-11-05 16:58:21

RabbitMQ订单超时取消延迟队列

2024-10-16 09:29:30

RabbitMQ延迟队列

2021-03-03 08:13:57

模式垃圾回收

2021-09-11 07:32:15

Java线程线程池

2010-02-24 11:19:00

Python主线程

2021-03-10 07:20:44

数据定位匹配

2021-05-26 11:30:24

Java线程池代码

2022-09-29 09:35:56

线程池

2021-02-03 08:12:23

函数委托Dotnet

2021-06-02 08:07:59

LinuxService应用

2023-12-11 18:18:24

Python编程线程

2011-04-21 09:13:14

并行计算

2021-07-07 08:01:51

命令行Dotnet Core控制台

2011-04-20 17:15:21

并行计算

2021-09-06 10:22:47

匿名对象编程

2020-07-09 10:03:07

Android 协程开发
点赞
收藏

51CTO技术栈公众号