首页 > 其他分享 >为什么需要[EnumeratorCancellation]?

为什么需要[EnumeratorCancellation]?

时间:2024-11-18 10:29:35浏览次数:1  
标签:为什么 需要 Console 迭代 取消 Task EnumeratorCancellation WriteLine WithCancellation

为什么需要 [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. 测试1:先取消方法参数 (ctsParam)

    • 在第3秒时,ctsParam 被取消。
    • 迭代器检测到 externalCancellationToken 被取消,抛出 OperationCanceledException
    • 终止迭代过程,即使 ctsWith 还未被取消。
  2. 测试2:先取消 WithCancellation (ctsWith)

    • 在第3秒时,ctsWith 被取消。
    • 迭代器检测到 cancellationToken 被取消,抛出 OperationCanceledException
    • 终止迭代过程,即使 ctsParam 还未被取消。

关键点:

  • 独立生效:无论是通过方法参数传递的 CancellationToken 还是通过 WithCancellation 传递的 CancellationToken,只要其中一个被取消,迭代器就会响应取消请求并终止迭代。

  • 取消顺序无关紧要:不论先取消哪一个取消令牌,迭代器都会正确响应取消请求。取消操作的顺序不会影响最终的效果。

总结

通过上述示例,我们深入了解了 [EnumeratorCancellation] 的必要性及其在异步迭代器中的核心作用。简要回顾:

  • 消除警告:使用 [EnumeratorCancellation] 可以消除 Visual Studio 提示的警告,确保取消请求能够正确传递给异步迭代器方法。

  • 职责分离:它实现了生产者与消费者的职责分离,使生产者专注于数据生成,消费者控制取消逻辑,从而提升代码的可维护性和可复用性。

  • 灵活的取消机制:即使同时传递多个取消令牌,只要任意一个被取消,迭代器就会终止,提供了灵活而强大的取消控制能力。

.NET 的这些强大功能为开发者提供了极大的便利和灵活性,使得编写高效、可维护的异步代码变得更加轻松与自信。让我们为 .NET 的强大功能自豪,并在实际开发中善加利用这些工具,构建出更优秀的软件解决方案!

标签:为什么,需要,Console,迭代,取消,Task,EnumeratorCancellation,WriteLine,WithCancellation
From: https://www.cnblogs.com/sdcb/p/18551982/why-we-need-enumerator-cancellation

相关文章

  • 一楼防潮施工是防止地面湿气对建筑物结构和室内环境的影响,施工时需要选择合适的防潮材
    一楼防潮施工标准主要是为了防止地下水、地面湿气对一楼结构和室内环境造成影响。尤其是在潮湿地区或地下水位较高的地区,防潮工程尤为重要。以下是关于一楼防潮施工标准的详细介绍。1. 防潮设计要求(1)防潮设计的依据《建筑防水工程质量验收规范》GB50208《地下工程防水技术......
  • 为什么选择UniApp而非原生开发?
    1.减少开发成本和时间1.1一套代码,多平台发布UniApp的最大优势在于跨平台能力,它允许开发者使用一套代码同时部署到多个平台,包括iOS、Android、Web以及各种小程序(如微信、支付宝、百度等)。相比于原生开发需要为每个平台编写单独的代码,UniApp大大减少了开发的工作量和时间。对于......
  • Solid 之旅 —— 为什么 props 被解构后会导致响应式丢失
    在前面的文章中,我们学习了Solid的响应式原理,深入了了解其实现方式。Solid之旅——Signal响应式原理这篇文章将主要深入解析组件内部props的原理,为什么结构后会导致响应式丢失?案例我们以一个例子作为参考,由浅入深的讲解其中的奥秘。Parent.tsximport{createS......
  • 织梦网站修改需要权限吗,如何获取织梦网站修改权限
    联系网站管理员如果您不是网站的管理员,需要联系网站管理员或拥有管理员权限的人员,请求他们授予您修改权限。登录织梦后台使用管理员账号登录到织梦后台管理界面。进入用户管理在后台管理界面中,找到并点击“系统”-“用户管理”选项。添加或编辑用户如果需要......
  • 为什么PN结中高掺杂的情况下,耗尽层的宽度会更窄
    刚学的时候这个问题困扰了我很长时间,通过查阅很多资料,找到了一个相对详细且合理的解释,加上自己的一些思考,现在,算是差不多明白了,所以,写下这篇文章,为以后同样困惑于这个问题的同学提供一个思路。首先,提出这样的问题的同学,可以肯定的是你很细心,发现了这个点,但是现在的很多视频和......
  • 如果要使晶体管(三极管)处于放大区,则需要满足条件之一就是UCE>UBE,探讨其原因。
    下面就是解释的内容:晶体管的基本结构和工作原理晶体管主要由三个区域组成:发射极(E)、基极(B)和集电极(C)。在NPN型晶体管中,发射极和集电极分别是N型和P型半导体,而基极是P型或N型半导体,位于两者之间。1.发射结(E-B结)当发射结正向偏置时(UBE>0),发射极的电子会被推向基极。这是因为......
  • AI对口型视频生成工具需要魔法
    探索未来:HedraAI对口型视频生成工具的革命在数字媒体的浪潮中,人工智能技术正以前所未有的速度改变着内容创作的方式。Hedra,这个由原斯坦福大学研究团队成立的数字创作实验室推出的AI对口型视频生成工具,正是这一变革的先锋。它专注于将AI技术应用于人物角色视频的生成,为数字......
  • axios的post请求,数据为什么要用qs处理?什么时候不用?
    在使用Axios进行HTTP请求时,特别是在进行POST请求时,是否需要对数据进行qs(Querystring)处理主要取决于后端API接收数据的格式(Content-Type)以及你的具体需求。为什么有时需要用qs处理数据?后端期望application/x-www-form-urlencoded格式的数据:如果后端API设计为接收application/......
  • AI时代下,哪些工作是无法替代的?你需要了解的三大核心领域
    文章目录前言一、创造性的工作:AI的边界在哪里?二、情感劳动:AI无法触及的人类温度三、复杂决策与战略规划:AI的局限性未来不可或缺的两大技能总结前言随着人工智能(AI)技术的飞速发展,许多行业和职业正面临前所未有的变革。自动化和AI系统的普及显然将重塑大量......
  • 为什么 Vue3 封装 Table 组件丢失 expose 方法呢?
    在实际开发中,我们通常会将某些常见组件进行二次封装,以便更好地实现特定的业务需求。然而,在封装Table组件时,遇到一个问题:Table内部暴露的方法,在封装之后的组件获取不到。代码展示为:constMyTable=defineComponent({name:'MyTable',props:ElTable.props,emits:......