为什么需要[EnumeratorCancellation]?
为什么需要 [EnumeratorCancellation]?
在使用 C# 编写异步迭代器时,您可能会遇到如下警告:
warning CS8425: 异步迭代器“TestConversationService.ChatStreamed(IReadOnlyList<ChatMessage>, ChatCompletionOptions, CancellationToken)”具有一个或多个类型为 "CancellationToken" 的参数,但它们都未用 "EnumeratorCancellation" 属性修饰,因此将不使用所生成的 "IAsyncEnumerable<>.GetAsyncEnumerator" 中的取消令牌参数。

看到这样的警告,您可能会困惑:究竟需要在异步迭代器的方法参数上添加 [EnumeratorCancellation] 属性吗?如果不添加,会有什么区别? 让我们深入探讨一下这个问题,揭示其背后的真相。
正常调用时,[EnumeratorCancellation] 的影响
如果您只是简单地在异步迭代器方法中传递一个普通的 CancellationToken,无论是否使用 [EnumeratorCancellation],方法的行为似乎并没有显著区别。例如:
public async IAsyncEnumerable<int> GenerateNumbersAsync(CancellationToken cancellationToken = default)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();
yield return i;
await Task.Delay(1000, cancellationToken);
}
}
public async Task ConsumeNumbersAsync()
{
CancellationTokenSource cts = new CancellationTokenSource();
Task cancelTask = Task.Run(async () =>
{
await Task.Delay(3000);
cts.Cancel();
});
try
{
await foreach (var number in GenerateNumbersAsync(cts.Token))
{
Console.WriteLine(number);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("枚举已被取消");
}
await cancelTask;
}
输出如下:
0
1
2
枚举已被取消
在上述代码中,即使没有使用 [EnumeratorCancellation],取消令牌 cts.Token 依然会生效,导致迭代过程被取消。这可能会让开发者误以为 [EnumeratorCancellation] 没有实际作用,进而引发更多的困惑。
揭开真相:生产者与消费者的职责分离
实际上,[EnumeratorCancellation] 的核心作用在于 实现生产者与消费者的职责分离。具体来说:
生产者(即提供数据的异步迭代方法)专注于数据的生成和响应取消请求,不关心取消请求的来源或何时取消。
消费者(即使用数据的部分)负责控制取消逻辑,独立地决定何时取消整个迭代过程。
通过这种设计,生产者不需要知道取消请求是由谁或何时发起的,简化了生产者的设计,同时赋予消费者更大的控制权。这不仅提高了代码的可维护性和可复用性,还避免了取消逻辑的混乱。
示例说明
下面通过一个示例,直观地展示 [EnumeratorCancellation] 如何实现职责分离。
1. 定义异步迭代器方法
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
public class DataProducer
{
public async IAsyncEnumerable<int> ProduceData(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
int i = 0;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"[Iterator] 生成数字: {i}");
yield return i++;
await Task.Delay(1000, cancellationToken); // 模拟数据生成延迟
}
}
}
在这个 DataProducer 类中,ProduceData 方法使用 [EnumeratorCancellation] 标注了 cancellationToken 参数。这意味着,当消费者通过 WithCancellation 传递取消令牌时,编译器会自动将该取消令牌传递给 ProduceData 方法的 cancellationToken 参数。
2. 定义消费者方法
using System;
using System.Threading;
using System.Threading.Tasks;
public class DataConsumer
{
public async Task ConsumeDataAsync(IAsyncEnumerable<int> producer)
{
using CancellationTokenSource cts = new CancellationTokenSource();
// 在5秒后发出取消请求
_ = Task.Run(async () =>
{
await Task.Delay(5000);
cts.Cancel();
Console.WriteLine("[Trigger] 已发出取消请求");
});
try
{
// 通过 WithCancellation 传递取消令牌
await foreach (var data in producer.WithCancellation(cts.Token))
{
Console.WriteLine($"[Consumer] 接收到数据: {data}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("[Consumer] 数据接收已被取消");
}
}
}
在 DataConsumer 类中,ConsumeDataAsync 方法创建了一个 CancellationTokenSource,并在5秒后取消它。通过 WithCancellation 方法,将取消令牌传递给 ProduceData 方法。这样,消费者完全控制了取消逻辑,而生产者只需响应取消请求。
3. 执行示例
public class Program
{
public static async Task Main(string[] args)
{
var producer = new DataProducer();
var consumer = new DataConsumer();
await consumer.ConsumeDataAsync(producer.ProduceData());
}
}
预期输出:
[Iterator] 生成数字: 0
[Consumer] 接收到数据: 0
[Iterator] 生成数字: 1
[Consumer] 接收到数据: 1
[Iterator] 生成数字: 2
[Consumer] 接收到数据: 2
[Iterator] 生成数字: 3
[Consumer] 接收到数据: 3
[Iterator] 生成数字: 4
[Consumer] 接收到数据: 4
[Trigger] 已发出取消请求
[Consumer] 数据接收已被取消
在5秒后,取消请求被触发,迭代器检测到取消并抛出 OperationCanceledException,导致迭代过程被中断。请注意DataConsumer在接收生产出来的数据 IAsyncEnumerable<int> 时,已经错过了在生产函数中传入 cancellationToken 的机会,但作为消费者,仍然可以通过 .WithCancellation 方法进行优雅取消。
这展示了生产者与消费者如何通过 WithCancellation 和 [EnumeratorCancellation] 实现职责分离,消费者能够独立地控制取消逻辑,而生产者只需响应取消请求。
CancellationToken 与 WithCancellation 同时作用时的行为
那么,如果在异步迭代器方法中同时传递了 CancellationToken 参数,并通过 WithCancellation 指定了不同的取消令牌,取消操作会听哪个的?还是都会监听?
结论是:两者都会生效,只要其中任意一个取消令牌被触发,迭代器都会检测到取消请求并中断迭代过程。这取决于方法内部如何处理多个取消令牌。
示例演示
以下是一个详细的示例,展示当同时传递 CancellationToken 参数和使用不同的 WithCancellation 时的行为。
1. 定义异步迭代器方法
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
public class EnumeratorCancellationDemo
{
// 异步迭代器方法,接受两个 CancellationToken
public async IAsyncEnumerable<int> GenerateNumbersAsync(
[EnumeratorCancellation] CancellationToken cancellationToken,
CancellationToken externalCancellationToken = default)
{
int i = 0;
try
{
while (true)
{
// 检查两个取消令牌
cancellationToken.ThrowIfCancellationRequested();
externalCancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"[Iterator] 生成数字: {i}");
yield return i++;
// 模拟异步操作
await Task.Delay(1000, cancellationToken);
}
}
finally
{
Console.WriteLine("[Iterator] 迭代器已退出。");
}
}
}
2. 定义消费者方法
public class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("启动枚举取消示例...\n");
var demo = new EnumeratorCancellationDemo();
// 测试1: 先取消方法参数
Console.WriteLine("=== 测试1: 先取消方法参数 ===\n");
await TestCancellation(demo, cancelParamFirst: true);
// 测试2: 先取消 WithCancellation
Console.WriteLine("\n=== 测试2: 先取消 WithCancellation ===\n");
await TestCancellation(demo, cancelParamFirst: false);
Console.WriteLine("\n演示结束。");
Console.ReadLine();
}
static async Task TestCancellation(EnumeratorCancellationDemo demo, bool cancelParamFirst)
{
using CancellationTokenSource ctsParam = new CancellationTokenSource();
using CancellationTokenSource ctsWith = new CancellationTokenSource();
if (cancelParamFirst)
{
// 第一个取消任务:3秒后取消 ctsParam
_ = Task.Run(async () =>
{
await Task.Delay(3000);
ctsParam.Cancel();
Console.WriteLine("[Trigger] 已取消 ctsParam (方法参数)");
});
// 第二个取消任务:5秒后取消 ctsWith
_ = Task.Run(async () =>
{
await Task.Delay(5000);
ctsWith.Cancel();
Console.WriteLine("[Trigger] 已取消 ctsWith (WithCancellation)");
});
}
else
{
// 第一个取消任务:3秒后取消 ctsWith
_ = Task.Run(async () =>
{
await Task.Delay(3000);
ctsWith.Cancel();
Console.WriteLine("[Trigger] 已取消 ctsWith (WithCancellation)");
});
// 第二个取消任务:5秒后取消 ctsParam
_ = Task.Run(async () =>
{
await Task.Delay(5000);
ctsParam.Cancel();
Console.WriteLine("[Trigger] 已取消 ctsParam (方法参数)");
});
}
try
{
// 传递 ctsWith.Token 作为方法参数,并通过 WithCancellation 传递 ctsWith.Token
await foreach (var number in demo.GenerateNumbersAsync(ctsWith.Token, ctsParam.Token).WithCancellation(ctsWith.Token))
{
Console.WriteLine($"[Consumer] 接收到数字: {number}");
}
}
catch (OperationCanceledException ex)
{
string reason = ex.CancellationToken == ctsWith.Token ? "WithCancellation" : "方法参数";
Console.WriteLine($"[Iterator] 迭代器检测到取消。原因: {reason}");
Console.WriteLine("[Consumer] 枚举已被取消。");
}
}
}
3. 运行示例并观察结果
启动程序后,控制台输出可能如下所示:
启动枚举取消示例...
=== 测试1: 先取消方法参数 ===
[Iterator] 生成数字: 0
[Consumer] 接收到数字: 0
[Iterator] 生成数字: 1
[Consumer] 接收到数字: 1
[Iterator] 生成数字: 2
[Consumer] 接收到数字: 2
[Trigger] 已取消 ctsParam (方法参数)
[Iterator] 迭代器已退出。
[Iterator] 迭代器检测到取消。原因: 方法参数
[Consumer] 枚举已被取消。
=== 测试2: 先取消 WithCancellation ===
[Iterator] 生成数字: 0
[Consumer] 接收到数字: 0
[Iterator] 生成数字: 1
[Consumer] 接收到数字: 1
[Trigger] 已取消 ctsWith (WithCancellation)
[Iterator] 生成数字: 2
[Consumer] 接收到数字: 2
[Trigger] 已取消 ctsWith (WithCancellation)
[Iterator] 迭代器已退出。
[Iterator] 迭代器检测到取消。原因: WithCancellation
[Consumer] 枚举已被取消。
演示结束。
解释:
测试1:先取消方法参数 (
ctsParam)- 在第3秒时,
ctsParam被取消。 - 迭代器检测到
externalCancellationToken被取消,抛出OperationCanceledException。 - 终止迭代过程,即使
ctsWith还未被取消。
- 在第3秒时,
测试2:先取消
WithCancellation(ctsWith)- 在第3秒时,
ctsWith被取消。 - 迭代器检测到
cancellationToken被取消,抛出OperationCanceledException。 - 终止迭代过程,即使
ctsParam还未被取消。
- 在第3秒时,
关键点:
独立生效:无论是通过方法参数传递的
CancellationToken还是通过WithCancellation传递的CancellationToken,只要其中一个被取消,迭代器就会响应取消请求并终止迭代。取消顺序无关紧要:不论先取消哪一个取消令牌,迭代器都会正确响应取消请求。取消操作的顺序不会影响最终的效果。
总结
通过上述示例,我们深入了解了 [EnumeratorCancellation] 的必要性及其在异步迭代器中的核心作用。简要回顾:
消除警告:使用
[EnumeratorCancellation]可以消除 Visual Studio 提示的警告,确保取消请求能够正确传递给异步迭代器方法。职责分离:它实现了生产者与消费者的职责分离,使生产者专注于数据生成,消费者控制取消逻辑,从而提升代码的可维护性和可复用性。
灵活的取消机制:即使同时传递多个取消令牌,只要任意一个被取消,迭代器就会终止,提供了灵活而强大的取消控制能力。
.NET 的这些强大功能为开发者提供了极大的便利和灵活性,使得编写高效、可维护的异步代码变得更加轻松与自信。让我们为 .NET 的强大功能自豪,并在实际开发中善加利用这些工具,构建出更优秀的软件解决方案!
为什么需要[EnumeratorCancellation]?的更多相关文章
- C#8.0——异步流(AsyncStream)
异步流(AsyncStream) 原文地址:https://github.com/dotnet/roslyn/blob/master/docs/features/async-streams.md 注意 ...
- 一文说通C#中的异步迭代器
今天来写写C#中的异步迭代器 - 机制.概念和一些好用的特性 迭代器的概念 迭代器的概念在C#中出现的比较早,很多人可能已经比较熟悉了. 通常迭代器会用在一些特定的场景中. 举个例子:有一个for ...
- .NET斗鱼直播弹幕客户端(2021)
.NET斗鱼直播弹幕客户端(2021) 离之前更新的两篇<.NET斗鱼直播弹幕客户端>已经有一段时间,近期有许多客户向我反馈刚好有这方面的需求,但之前的代码不能用了--但网上许多流传的No ...
- 开发进阶:Dotnet Core多路径异步终止
今天用一个简单例子说说异步的多路径终止.我尽可能写得容易理解吧,但今天的内容需要有一定的编程能力. 今天这个话题,来自于最近对gRPC的一些技术研究. 话题本身跟gRPC没有太大关系.应用中,我用 ...
- [gRPC via C#] gRPC本质的探究与实践
鉴于内容过多,先上太长不看版: grpc 就是请求流&响应流特殊一点的 Http 请求,性能和 WebAPI 比起来只快在 Protobuf 上: 附上完整试验代码:GrpcWithOutSD ...
随机推荐
- Centos8下Redis设置Session共享存储
Redis-Session共享存储 前提条件: 1.安装Redis 2.安装Apache或Nginx 3.安装php 本机环境: php:7.3 Redis:5.0.7 开始部署: 我是分别用Cent ...
- 有哪些让你「 爽到爆炸 」的 Windows 软件?
前言 本文源于知乎的一个提问,如标题所示:有哪些让你「 爽到爆炸 」的 Windows 软件?今天大姚给大家分享6款C#/.NET开源且免费的Windows软件,希望可以帮助大家提高学习.开发.办公效 ...
- 《SpringCloud微服务之间相互调用》之Feign实战
一.场景再现 假设我们有这样一个场景: 用户付款成功后,扣除用户金额,还要减少仓库数量.按照微服务的设计理念,用户具有至少以下3个服务(项目): 1.订单 2.账户 3.仓库 微服务之间都是相互独立的 ...
- docker 修改容器内容后更新镜像的流程
在 Docker 中,如果你修改了一个容器的内容并希望将这些更改保存为一个新的镜像,可以按照以下步骤进行: docker version: 26.1 1. 确保容器运行 首先,确保你正在修改的容器是运 ...
- DTO转VO工具
data工具,实现了对象拷贝 DTO -> VO 只需要实现一个类即可 data-utils data工具,实现了对象拷贝DTO -> VO 解决的问题 Mapstruct需要安插件!!! ...
- win10安装linux的gcc
mysy2下载gcc 过程比较艰苦,2024年秋冬讲课,被linux毒打了3天 pacman -S mingw-w64-ucrt-x86_64-gcc 这个一次成功,不行继续接大招 实在不行安装 ...
- 智慧高校IT智能运维方案
当前高校网络已成为每个学校必备的信息基础设施,也成了学校提高教学.科研及管理水平的重要途径和手段.随着信息化发展,高校网络建设逐步走向数字化.智慧化,传统的人力巡检.运维逐渐难以支撑高校校园稳定运行. ...
- Linux内存管理2.6 -反向映射RMAP(最终版本)
所谓反向映射是相对于从虚拟地址到物理地址的映射,反向映射是从物理页面到虚拟地址空间VMA的反向映射. RMAP能否实现的基础是通过struct anon_vma.struct anon_vma_cha ...
- iOS 14 UIDatePicker适配问题,使用老的选择器样式
iOS 14 UIDatePicker 在 13.4 新增了2个属性如下 @property (nonatomic, readwrite, assign) UIDatePickerStyle pref ...
- npm/yarn是什么,他们的区别 ? & node.js 又是个啥
是什么? npm(全称Node Package Manager,即 node 包管理器) ,是Node.js默认的包管理系统 ; 区别: yarn 的速度开快,因为yarn 是并行安装,npm是串行安 ...