11

.NET Core 取消令牌:CancellationToken

 3 years ago
source link: https://beckjin.com/2020/11/08/aspnet-cancellationtoken/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在 .NET 开发中,CancellationToken(取消令牌)是一项比较重要的功能,掌握并合理的使用 CancellationToken 可以提升服务的性能。特别在异步编程中,我常常会以创建 Task 的方式利用多线程执行一些耗时或非核心业务逻辑,表面上看既提高了整个流程的执行速度,又充分利用了服务器资源。然而类似 Task 的方式如果没设置过取消令牌,一旦开启,是无法被外部取消的,所以当主线程出异常或被提前终止时,已开启的异步线程其实依然在执行,这时对服务器资源可能是一种浪费,而 CancellationToken 就可以对这类情况进行一定的补救。

下面通过几种常见的使用场景来介绍 CancellationToken。

在 HttpClient 中的使用

HttpClient 是开发中比较常用的一个组件,关于超时可通过 Timeout 参数进行设置,其实它也是可以通过配置 CancellationToken 来实现超时定义,使用 CancellationToken 的最大好处是调用链共享此令牌状态,状态变更时会自动做出响应。

1
2
3
4
5
6
7
public async Task<string> GetHomeAsync(CancellationToken cancellationToken = default)
{
var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync("https://github.com/", cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}


1
2
3
4
5
6
public async Task<string> GetGithubHome()
{
var cts = new CancellationTokenSource(1000);
var result = await _githubService.GetHomeAsync(cts.Token);
return result;
}

Github 一般访问会比较慢,可通过设置 1s 演示效果:

httpClient.png

在 gRPC 中的使用

通过 VS 的 gRPC 服务模板创建一个 gRPC 服务端(如果对 gRPC 的使用还不太了解,参考官方文档 玩起来吧),服务端主要提供一个获取用户列表 (GetList) 的接口。实现如下,_userRepository 内部是基于 MongoDB 实现的查询用户数据,对应使用的 MongoDB.Driver 提供的方法默认已支持设置 CancellationToken,所以这里直接引用 ServerCallContext 上下文中的 CancellationToken,而此 CancellationToken 又是从客户端传递来的,所以 CancellationToken 将作用于整个调用链中。另外如果在客户端动态取消了此令牌,服务器也将会收到通知。

1
2
3
4
5
6
7
8
9
10
11
public override async Task<GetListReply> GetList(GetListRequest request, ServerCallContext context)
{
await Task.Delay(1000); // 模拟效果,服务端停1s
var users = await _userRepository.GetListAsync(context.CancellationToken);
var reply = new GetListReply();
foreach (var item in users)
{
reply.Users.Add(new UserModel { UserId = item.UserId, Name = item.Name });
}
return reply;
}

Client 端主要代码如下,在接口层创建了 CancellationTokenSource 对象,并设置了令牌的过期时间,即在发起远程调用时,如果 1s 内没返回,那就取消这个调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserService : IUserService
{
private readonly UserClient _client;

public UserService()
{
var channel = GrpcChannel.ForAddress("https://localhost:5001");
_client = new UserClient(channel);
}

public async Task<GetListReply> GetListAsync(CancellationToken cancellationToken = default)
{
return await _client.GetListAsync(new GetListRequest(), cancellationToken: cts.Token);
}
}


1
2
3
4
5
6
7
[HttpGet]
public async Task<string> GetUserList()
{
var cts = new CancellationTokenSource(1000);
var result = await _userService.GetListAsync(cts.Token);
return JsonConvert.SerializeObject(result.Users);
}
grpc.png

在 WebAPI 中的使用

前端调用后端的接口一般是基于 Ajax 来实现,当浏览器网页被 连续 F5 刷新页面加载中被停止Ajax 请求被主动 abort 时,控制台 network 看板中会出现一些状态为 canceled 的请求,如下:

canceledrequest.png

对于这类请求,客户端虽然主动放弃了,如果服务端没有相应处理,其实接口对应的后端程序还是在不停的执行,只是这个执行结果不会被使用而已,所以这其实是非常浪费服务器资源的。

实际上浏览器取消请求时,服务端会将 HttpContext.RequestAborted 中的 Token 绑定到 Action 的 CancellationToken 参数。我们只需在接口中增加参数 CancellationToken,并将其传入其他接口调用中,程序识别到令牌被取消就会自动放弃继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[HttpGet]
public async Task<string> Index(CancellationToken cancellationToken)
{
try
{
await _userService.GetListAsync(cancellationToken);
await Task.Delay(5000); // 等待期间,取消请求(Postman 即可模拟)
await _githubService.GetHomeAsync(cancellationToken);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
}

return "ok";
}
webapi.png

对于 WebAPI 接口被取消调用的场景,特别是对于查询功能的接口,CancellationToken 的传递就显得尤为必要了,它能减少很多底层服务接口的无效调用。

最后针对取消令牌产生的异常需要收尾干净,一般像 WebAPI 项目可以使用自带的过滤器或具有 AOP 功能的组件,gRPC 服务可使用自带的拦截器功能。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK