【发布时间】:2020-02-02 16:42:40
【问题描述】:
编辑:这个问题的要求已经改变。请参阅下面的更新部分。
我有一个异步迭代器方法,它产生一个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
预期的输出是TaskCanceledException 出现在数字 5 之后。看来我误解了 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 是解决此问题所需的扩展方法。
【问题讨论】:
-
如果代码的编写方式不允许提前中止,则不能强制提前中止。好吧,你可以,但你真的不应该。
-
@LasseVågsætherKarlsen 这就像在说你不应该尽早退出循环。这是一个非常强烈的主张!
-
情况并不相似——中断同步循环总是安全的,但仅在迭代之间“取消”异步枚举意味着我们可能会增加相当大的开销和延迟(@987654341 不是问题@,但对于实际工作来说绝对是一个问题)。这种情况并不像一般异步工作那样可怕(我们可能不得不接受工作根本没有被取消并且仍在后台进行,尽管被忽略了),因为异步枚举隐含地包括处理资源,但仍然不是最佳的.将其与
Task.Delay(10000)进行比较。 -
@JeroenMostert 打破同步循环是安全的,因为编译器生成的迭代器 are disposing properly all disposable resources,编译器生成的异步迭代器也是如此。当你在
await foreach内部中断时意味着你在前一个MoveNextAsync 完成后中断,此时没有什么特别的事情发生。 -
@JeroenMostert 关于忽略后台工作的情况,我提出了一个相关问题here。我得到的反馈是,除了打破循环之外,我还应该将责任转移给调用者以提供额外的取消通知。
标签: c# cancellation c#-8.0 iasyncenumerable