编辑:此问题的要求已更改。请参阅下面的更新部分。
我有一个异步迭代器方法,该方法生成一个IAsyncEnumerable<int>
(数字流),每200毫秒一个数字。此方法的调用方使用流,但要在1000毫秒后停止枚举。因此,使用CancellationTokenSource
,并且令牌作为
WithCancellation
扩展方法的参数。但是令牌不被尊重。枚举继续进行,直到所有数字都被消耗为止:
static async IAsyncEnumerable<int> GetSequence()
{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(200);
yield return i;
}
}
var cts = new CancellationTokenSource(1000);
await foreach (var i in GetSequence().WithCancellation(cts.Token))
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} > {i}");
}
输出:
12:55:17.506> 1
12:55:17.739> 2
12:55:17.941> 3
12:55:18.155> 4
12:55:18.367> 5
12:55:18.570> 6
12:55:18.772> 7
12:55:18.973> 8
12:55:19.174> 9
12:55:19.376> 10
预期的输出是在数字5之后出现的TaskCanceledException
。似乎我误解了WithCancellation
的实际功能。该方法仅将提供的令牌传递给迭代器方法(如果该方法接受一个)。否则,就像在我的示例中使用方法GetSequence()
一样,令牌将被忽略。我想我的解决方案是手动查询枚举主体内的令牌:
var cts = new CancellationTokenSource(1000);
await foreach (var i in GetSequence())
{
cts.Token.ThrowIfCancellationRequested();
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} > {i}");
}
这很简单,效果很好。但是无论如何,我想知道是否有可能创建一个扩展方法来实现我期望的WithCancellation
,以烘烤随后枚举中的令牌。这是所需方法的签名:
public static IAsyncEnumerable<T> WithEnforcedCancellation<T>(
this IAsyncEnumerable<T> source, CancellationToken cancellationToken)
{
// Is it possible?
}
更新:似乎当我问这个问题时,我对整个取消概念的目的有不正确的理解。我给人的印象是,取消的目的是打破MoveNextAsync
的等待中的 循环,而真正的目的是取消等待中的 。在我的琐碎示例中,等待仅持续200毫秒,但是在实际示例中,等待可能会更长,甚至是无限的。意识到这一点之后,我目前的问题几乎没有任何价值,我必须删除它并打开一个具有相同标题的新问题,或者更改现有问题的要求。两种选择都以一种或另一种方式是不好的。
我决定采用第二种选择。因此,我不接受当前接受的答案,我正在寻求一种新的解决方案,以解决更棘手的问题,即以立即生效的方式实施取消。换句话说,取消令牌应导致在大约毫秒内完成异步枚举。让我们举一个实际的例子来区分期望和不期望的行为:
var cts = new CancellationTokenSource(500);
var stopwatch = Stopwatch.StartNew();
try
{
await foreach (var i in GetSequence().WithEnforcedCancellation(cts.Token))
{
Console.WriteLine($"{stopwatch.Elapsed:m':'ss'.'fff} > {i}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine($"{stopwatch.Elapsed:m':'ss'.'fff} > Canceled");
}
输出(理想):
0:00.242> 1
0:00.467> 2
0:00.500>已取消
输出(不希望的):
0:00.242> 1
0:00.467> 2
0:00.707>已取消
GetSequence
与初始示例中的方法相同,每200毫秒流送一个数字。此方法不支持取消,前提是我们无法更改。 WithEnforcedCancellation
是解决此问题的必需扩展方法。
答案 0 :(得分:5)
IAsyncEnumerable
通过EnumeratorCancellation
属性为该机制明确提供:
static async IAsyncEnumerable<int> GetSequence([EnumeratorCancellation] CancellationToken ct = default) {
for (int i = 1; i <= 10; i++) {
ct.ThrowIfCancellationRequested();
await Task.Delay(200); // or `Task.Delay(200, ct)` if this wasn't an example
yield return i;
}
}
实际上,如果为该方法提供一个CancellationToken
参数,但不添加该属性,则编译器足以发出警告。
请注意,传递给.WithCancellation
的令牌将覆盖传递给方法的所有本地令牌。 specs对此有详细说明。
当然,这仅在枚举实际上接受CancellationToken
的情况下仍然有效-但是任何async
工作都只有在合作完成的情况下取消才有效。 Yeldar's answer有利于“强制”将取消措施引入不支持它的枚举,但是首选解决方案应该是修改枚举以单独支持取消—编译器会尽一切努力为您提供帮助
答案 1 :(得分:2)
您可以将逻辑提取到如下扩展方法中:
public static async IAsyncEnumerable<T> WithEnforcedCancellation<T>(
this IAsyncEnumerable<T> source, CancellationToken cancellationToken)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
cancellationToken.ThrowIfCancellationRequested();
await foreach (var item in source)
{
cancellationToken.ThrowIfCancellationRequested();
yield return item;
}
}