在使用 C# 編寫異步迭代器時,您可能會遇到如下警告:
warning CS8425: 異步迭代器“TestConversationService.ChatStreamed(IReadOnlyList<ChatMessage>, ChatCompletionOptions, CancellationToken)”具有一個或多個類型為 "CancellationToken" 的參數(shù),但它們都未用 "EnumeratorCancellation" 屬性修飾,因此將不使用所生成的 "IAsyncEnumerable<>.GetAsyncEnumerator" 中的取消令牌參數(shù)。
看到這樣的警告,您可能會困惑:究竟需要在異步迭代器的方法參數(shù)上添加 [EnumeratorCancellation]
屬性嗎?如果不添加,會有什么區(qū)別? 讓我們深入探討一下這個問題,揭示其背后的真相。
正常調(diào)用時,[EnumeratorCancellation] 的影響
如果您只是簡單地在異步迭代器方法中傳遞一個普通的 CancellationToken
,無論是否使用 [EnumeratorCancellation]
,方法的行為似乎并沒有顯著區(qū)別。例如:
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
依然會生效,導(dǎo)致迭代過程被取消。這可能會讓開發(fā)者誤以為 [EnumeratorCancellation]
沒有實際作用,進(jìn)而引發(fā)更多的困惑。
揭開真相:生產(chǎn)者與消費者的職責(zé)分離
實際上,[EnumeratorCancellation]
的核心作用在于 實現(xiàn)生產(chǎn)者與消費者的職責(zé)分離。具體來說:
通過這種設(shè)計,生產(chǎn)者不需要知道取消請求是由誰或何時發(fā)起的,簡化了生產(chǎn)者的設(shè)計,同時賦予消費者更大的控制權(quán)。這不僅提高了代碼的可維護(hù)性和可復(fù)用性,還避免了取消邏輯的混亂。
示例說明
下面通過一個示例,直觀地展示 [EnumeratorCancellation]
如何實現(xiàn)職責(zé)分離。
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] 生成數(shù)字: {i}");
yield return i++;
await Task.Delay(1000, cancellationToken);
}
}
}
在這個 DataProducer
類中,ProduceData
方法使用 [EnumeratorCancellation]
標(biāo)注了 cancellationToken
參數(shù)。這意味著,當(dāng)消費者通過 WithCancellation
傳遞取消令牌時,編譯器會自動將該取消令牌傳遞給 ProduceData
方法的 cancellationToken
參數(shù)。
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();
_ = Task.Run(async () =>
{
await Task.Delay(5000);
cts.Cancel();
Console.WriteLine("[Trigger] 已發(fā)出取消請求");
});
try
{
await foreach (var data in producer.WithCancellation(cts.Token))
{
Console.WriteLine($"[Consumer] 接收到數(shù)據(jù): {data}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("[Consumer] 數(shù)據(jù)接收已被取消");
}
}
}
在 DataConsumer
類中,ConsumeDataAsync
方法創(chuàng)建了一個 CancellationTokenSource
,并在5秒后取消它。通過 WithCancellation
方法,將取消令牌傳遞給 ProduceData
方法。這樣,消費者完全控制了取消邏輯,而生產(chǎn)者只需響應(yīng)取消請求。
3. 執(zhí)行示例
public class Program
{
public static async Task Main(string[] args)
{
var producer = new DataProducer();
var consumer = new DataConsumer();
await consumer.ConsumeDataAsync(producer.ProduceData());
}
}
預(yù)期輸出:
[Iterator] 生成數(shù)字: 0
[Consumer] 接收到數(shù)據(jù): 0
[Iterator] 生成數(shù)字: 1
[Consumer] 接收到數(shù)據(jù): 1
[Iterator] 生成數(shù)字: 2
[Consumer] 接收到數(shù)據(jù): 2
[Iterator] 生成數(shù)字: 3
[Consumer] 接收到數(shù)據(jù): 3
[Iterator] 生成數(shù)字: 4
[Consumer] 接收到數(shù)據(jù): 4
[Trigger] 已發(fā)出取消請求
[Consumer] 數(shù)據(jù)接收已被取消
在5秒后,取消請求被觸發(fā),迭代器檢測到取消并拋出 OperationCanceledException
,導(dǎo)致迭代過程被中斷。請注意DataConsumer在接收生產(chǎn)出來的數(shù)據(jù) IAsyncEnumerable<int>
時,已經(jīng)錯過了在生產(chǎn)函數(shù)中傳入 cancellationToken
的機(jī)會,但作為消費者,仍然可以通過 .WithCancellation
方法進(jìn)行優(yōu)雅取消。
這展示了生產(chǎn)者與消費者如何通過 WithCancellation
和 [EnumeratorCancellation]
實現(xiàn)職責(zé)分離,消費者能夠獨立地控制取消邏輯,而生產(chǎn)者只需響應(yīng)取消請求。
CancellationToken 與 WithCancellation 同時作用時的行為
那么,如果在異步迭代器方法中同時傳遞了 CancellationToken
參數(shù),并通過 WithCancellation
指定了不同的取消令牌,取消操作會聽哪個的?還是都會監(jiān)聽?
結(jié)論是:兩者都會生效,只要其中任意一個取消令牌被觸發(fā),迭代器都會檢測到取消請求并中斷迭代過程。這取決于方法內(nèi)部如何處理多個取消令牌。
示例演示
以下是一個詳細(xì)的示例,展示當(dāng)同時傳遞 CancellationToken
參數(shù)和使用不同的 WithCancellation
時的行為。
1. 定義異步迭代器方法
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
public class EnumeratorCancellationDemo
{
public async IAsyncEnumerable<int> GenerateNumbersAsync(
[EnumeratorCancellation] CancellationToken cancellationToken,
CancellationToken externalCancellationToken = default)
{
int i = 0;
try
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
externalCancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"[Iterator] 生成數(shù)字: {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();
Console.WriteLine("=== 測試1: 先取消方法參數(shù) ===\n");
await TestCancellation(demo, cancelParamFirst: true);
Console.WriteLine("\n=== 測試2: 先取消 WithCancellation ===\n");
await TestCancellation(demo, cancelParamFirst: false);
Console.WriteLine("\n演示結(jié)束。");
Console.ReadLine();
}
static async Task TestCancellation(EnumeratorCancellationDemo demo, bool cancelParamFirst)
{
using CancellationTokenSource ctsParam = new CancellationTokenSource();
using CancellationTokenSource ctsWith = new CancellationTokenSource();
if (cancelParamFirst)
{
_ = Task.Run(async () =>
{
await Task.Delay(3000);
ctsParam.Cancel();
Console.WriteLine("[Trigger] 已取消 ctsParam (方法參數(shù))");
});
_ = Task.Run(async () =>
{
await Task.Delay(5000);
ctsWith.Cancel();
Console.WriteLine("[Trigger] 已取消 ctsWith (WithCancellation)");
});
}
else
{
_ = Task.Run(async () =>
{
await Task.Delay(3000);
ctsWith.Cancel();
Console.WriteLine("[Trigger] 已取消 ctsWith (WithCancellation)");
});
_ = Task.Run(async () =>
{
await Task.Delay(5000);
ctsParam.Cancel();
Console.WriteLine("[Trigger] 已取消 ctsParam (方法參數(shù))");
});
}
try
{
await foreach (var number in demo.GenerateNumbersAsync(ctsWith.Token, ctsParam.Token).WithCancellation(ctsWith.Token))
{
Console.WriteLine($"[Consumer] 接收到數(shù)字: {number}");
}
}
catch (OperationCanceledException ex)
{
string reason = ex.CancellationToken == ctsWith.Token ? "WithCancellation" : "方法參數(shù)";
Console.WriteLine($"[Iterator] 迭代器檢測到取消。原因: {reason}");
Console.WriteLine("[Consumer] 枚舉已被取消。");
}
}
}
3. 運行示例并觀察結(jié)果
啟動程序后,控制臺輸出可能如下所示:
啟動枚舉取消示例...
=== 測試1: 先取消方法參數(shù) ===
[Iterator] 生成數(shù)字: 0
[Consumer] 接收到數(shù)字: 0
[Iterator] 生成數(shù)字: 1
[Consumer] 接收到數(shù)字: 1
[Iterator] 生成數(shù)字: 2
[Consumer] 接收到數(shù)字: 2
[Trigger] 已取消 ctsParam (方法參數(shù))
[Iterator] 迭代器已退出。
[Iterator] 迭代器檢測到取消。原因: 方法參數(shù)
[Consumer] 枚舉已被取消。
=== 測試2: 先取消 WithCancellation ===
[Iterator] 生成數(shù)字: 0
[Consumer] 接收到數(shù)字: 0
[Iterator] 生成數(shù)字: 1
[Consumer] 接收到數(shù)字: 1
[Trigger] 已取消 ctsWith (WithCancellation)
[Iterator] 生成數(shù)字: 2
[Consumer] 接收到數(shù)字: 2
[Trigger] 已取消 ctsWith (WithCancellation)
[Iterator] 迭代器已退出。
[Iterator] 迭代器檢測到取消。原因: WithCancellation
[Consumer] 枚舉已被取消。
演示結(jié)束。
解釋:
測試1:先取消方法參數(shù) (ctsParam
)
- 在第3秒時,
ctsParam
被取消。 - 迭代器檢測到
externalCancellationToken
被取消,拋出 OperationCanceledException
。 - 終止迭代過程,即使
ctsWith
還未被取消。
測試2:先取消 WithCancellation
(ctsWith
)
- 在第3秒時,
ctsWith
被取消。 - 迭代器檢測到
cancellationToken
被取消,拋出 OperationCanceledException
。 - 終止迭代過程,即使
ctsParam
還未被取消。
關(guān)鍵點:
總結(jié)
通過上述示例,我們深入了解了 [EnumeratorCancellation]
的必要性及其在異步迭代器中的核心作用。簡要回顧:
消除警告:使用 [EnumeratorCancellation]
可以消除 Visual Studio 提示的警告,確保取消請求能夠正確傳遞給異步迭代器方法。
職責(zé)分離:它實現(xiàn)了生產(chǎn)者與消費者的職責(zé)分離,使生產(chǎn)者專注于數(shù)據(jù)生成,消費者控制取消邏輯,從而提升代碼的可維護(hù)性和可復(fù)用性。
靈活的取消機(jī)制:即使同時傳遞多個取消令牌,只要任意一個被取消,迭代器就會終止,提供了靈活而強(qiáng)大的取消控制能力。
轉(zhuǎn)自https://www.cnblogs.com/sdcb/p/18551982/why-we-need-enumerator-cancellation
該文章在 2024/11/19 8:44:55 編輯過