【发布时间】:2021-10-31 19:26:41
【问题描述】:
我正在编写一个返回类型为ValueTask<T> 并接受CancellationToken 的API。如果在调用该方法时CancellationToken 已经被取消,我想返回一个取消的ValueTask<T> (IsCanceled == true),它会在等待时传播OperationCanceledException。用异步方法做这件事很简单:
async ValueTask<int> MyMethod1(CancellationToken token)
{
token.ThrowIfCancellationRequested();
//...
return 13;
}
ValueTask<int> task = MyMethod1(new CancellationToken(true));
Console.WriteLine($"IsCanceled: {task.IsCanceled}"); // True
await task; // throws OperationCanceledException
我决定切换到非async 实现,但现在我无法重现相同的行为。将Task.FromCanceled 正确包装为已取消的ValueTask<T>,但异常类型为TaskCanceledException,这是不可取的:
ValueTask<int> MyMethod2(CancellationToken token)
{
if (token.IsCancellationRequested)
return new ValueTask<int>(Task.FromCanceled<int>(token));
//...
return new ValueTask<int>(13);
}
ValueTask<int> task = MyMethod2(new CancellationToken(true));
Console.WriteLine($"IsCanceled: {task.IsCanceled}"); // True
await task; // throws TaskCanceledException (undesirable)
另一个不成功的尝试是包装Task.FromException。这个传播了正确的异常类型,但任务是错误的而不是取消的:
ValueTask<int> MyMethod3(CancellationToken token)
{
if (token.IsCancellationRequested)
return new ValueTask<int>(
Task.FromException<int>(new OperationCanceledException(token)));
//...
return new ValueTask<int>(13);
}
ValueTask<int> task = MyMethod3(new CancellationToken(true));
Console.WriteLine($"IsCanceled: {task.IsCanceled}"); // False (undesirable)
await task; // throws OperationCanceledException
这个问题有什么解决方案吗,或者我应该接受我的 API 行为不一致,有时会传播TaskCanceledExceptions(当令牌已经取消时),有时会传播OperationCanceledExceptions(当令牌稍后被取消)?
更新:作为我试图避免的不一致的一个实际示例,这是一个来自内置 Channel<T> 类的示例:
Channel<int> channel = Channel.CreateUnbounded<int>();
ValueTask<int> task1 = channel.Reader.ReadAsync(new CancellationToken(true));
await task1; // throws TaskCanceledException
ValueTask<int> task2 = channel.Reader.ReadAsync(new CancellationTokenSource(100).Token);
await task2; // throws OperationCanceledException
第一个ValueTask<int> 抛出TaskCanceledException,因为令牌已经被取消。第二个ValueTask<int> 抛出OperationCanceledException,因为令牌在100 毫秒后被取消。
【问题讨论】:
-
@GSerg 在实际使用中可能无关紧要,但如果您通过其一致性评估 API 则很重要。目前我对此 API 有两个不同的单元测试,
Assert.ThrowsException<TaskCanceledException>(() =>用于立即取消,Assert.ThrowsException<OperationCanceledException>(() =>用于延迟取消,至少可以说看起来很笨重。 -
您可以使用断言库来处理将派生类型与指定异常类型匹配的情况。一个例子是 xUnit 的 Assert.ThrowAny
来处理你的两种情况 -
@Moho 老实说,我更担心我的 API 行为不一致,并且切换到提供更宽松断言的单元测试库不在我的优先级列表中。 :-)
标签: c# async-await task-parallel-library cancellation-token valuetask