.NET Core 取消令牌:CancellationToken

本文介绍了CancellationToken在.NET开发中的重要性,特别是在异步编程和多线程场景下,如何通过它来实现任务的取消和资源管理。举例说明了HttpClient和gRPC服务中如何配置CancellationToken以控制超时和响应客户端取消请求,以及在WebAPI接口中如何利用它处理被取消的请求,减少无效调用,优化服务器资源利用率。

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

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

在 HttpClient 中的使用

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

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();
}

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

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

在 gRPC 中的使用

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

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 内没返回,那就取消这个调用。

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);
  }
}

[HttpGet]
public async Task<string> GetUserList()
{
  var cts = new CancellationTokenSource(1000);
  var result = await _userService.GetListAsync(cts.Token);
  return JsonConvert.SerializeObject(result.Users);
}

在 WebAPI 中的使用

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

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

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

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

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



作者:BeckJin
链接:https://www.jianshu.com/p/f1b9960a8b39
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

<think>好的,我现在需要解释用户提供的C#代码示例,关于如何使用CancellationTokenSource和CancellationToken取消任务。让我先仔细看看代码的结构和各个部分的作用。 首先,用户创建了一个CancellationTokenSource实例,名为tokensource。这是用来生成取消令牌并控制取消信号的。然后,通过tokensource.Token获取关联的CancellationToken,赋值给cancellationToken变量。这一步应该是将令牌与源关联起来,以便后续传递到任务中。 接下来,用户使用Task.Run启动了一个新任务,任务内是一个无限循环的while(true)空循环。这里传入了cancellationToken作为参数。根据我的理解,Task.Run的重载可以接受一个CancellationToken,这样当任务开始执行前,如果令牌已经被取消,任务就不会执行,直接进入取消状态。但如果在任务执行过程中令牌取消,任务本身并不会自动停止,需要代码中主动检查令牌的状态。 然后,用户立即调用了tokensource.Cancel(),这应该会触发取消请求,设置令牌的状态为已取消。现在的问题是,任务中的循环并没有检查取消令牌,所以即使调用了Cancel,任务可能不会停止,导致无限循环一直执行下去,无法退出。 我需要确认Task.Run传入CancellationToken的作用。根据文档,当传递令牌给Task.Run时,主要的作用是在任务尚未开始执行时,如果令牌已经被取消,任务会被标记为取消。但一旦任务开始运行,令牌取消并不会自动终止任务,必须由任务内部的代码来响应取消请求,比如通过抛出OperationCanceledException或者定期检查IsCancellationRequested属性。 所以用户的代码中,任务里的循环没有对cancellationToken进行任何处理,因此即使调用了Cancel,任务也不会停止,会一直运行下去。这会导致资源泄漏,任务无法被正确取消。 正确的做法应该是在循环内部添加cancellationToken.ThrowIfCancellationRequested(),这样在每次循环迭代时会检查是否取消,如果已取消,会抛出异常,终止循环。或者可以定期检查IsCancellationRequested属性,手动退出循环。 另外,用户可能在示例中立即调用Cancel,可能在任务还没开始运行时就取消了,这时候任务可能根本不会启动。但在实际代码中,可能需要一定的延迟,或者在任务开始后再取消,这时候就需要任务内部有取消检查的机制。 总结来说,用户的代码展示了如何创建和传递取消令牌,但缺乏任务内部对取消的响应处理,因此无法正确取消任务。需要补充循环内的取消检查代码才能有效停止任务。</think>以下是关于你提供的C#代码的逐步解析和优化建议: #### 1. 代码结构解析 ```csharp CancellationTokenSource tokensource = new CancellationTokenSource(); //创建取消信号源 CancellationToken cancellationToken = tokensource.Token; //获取关联令牌 Task.Run(() => { while (true) { // 缺少取消检查! } }, cancellationToken); // 传入令牌 tokensource.Cancel(); //立即触发取消 ``` #### 2. 关键问题分析 - ✅ **令牌绑定正确性**:任务已正确关联取消令牌 - ❌ **缺失取消检查**:循环内未响应取消请求,导致任务无法终止 - ⚠️ **执行时序风险**:`Cancel()`可能早于任务启动,需注意线程调度问题 #### 3. 优化后的标准写法 ```csharp CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token; Task backgroundTask = Task.Run(() => { while (true) { // 推荐方式1:自动抛异常终止 token.ThrowIfCancellationRequested(); // 或方式2:手动检查退出 if (token.IsCancellationRequested) { Console.WriteLine("任务优雅退出"); break; } } }, token); // 模拟延迟后取消 await Task.Delay(1000); cts.Cancel(); try { await backgroundTask; } catch (OperationCanceledException) { Console.WriteLine("任务已取消"); } ``` #### 4. 核心机制说明表 | 组件 | 作用 | |-----------------------|----------------------------------------------------------------------| | `CancellationTokenSource` | 取消信号控制器,产生取消指令 | | `CancellationToken` | 轻量级取消信号接收器,线程安全 | | `ThrowIfCancellationRequested` | 自动检测取消状态并抛出`OperationCanceledException` | #### 5. 最佳实践建议 1. **及时释放资源**:对`CancellationTokenSource`使用`using`语句或手动调用`Dispose()` 2. **超时控制**:可使用`CancelAfter(毫秒)`实现自动超时取消 3. **组合令牌**:通过`CancellationTokenSource.CreateLinkedTokenSource()`实现多令牌监听 4. **异常处理**:务必捕获`OperationCanceledException`处理取消逻辑 #### 6. 执行流程图解 ``` [主线程] 创建CTS → 获取Token → 启动任务 → 执行业务逻辑 → 触发Cancel() ↓ [任务线程] ↑ 启动 → 循环检查Token → 收到信号 → 终止循环 → 清理资源 ``` 这个机制广泛应用于: - 长时间运行的后台任务 - Web请求取消(ASP.NET Core中常见) - 用户手动取消操作 - 分布式任务协调 需要特别注意:取消请求是协同式的,任务代码必须主动响应取消信号才能正确终止。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值