[英]How to return a canceled ValueTask<T> that propagates an OperationCanceledException, without async/await?
I am writting an API that has a ValueTask<T>
return type, and accepts a CancellationToken
.我正在编写具有ValueTask<T>
返回类型并接受CancellationToken
的 API 。 In case the CancellationToken
is already canceled upon invoking the method, I would like to return a canceled ValueTask<T>
( IsCanceled == true
), that propagates an OperationCanceledException
when awaited.如果CancellationToken
在调用该方法时已经被取消,我想返回一个取消的ValueTask<T>
( IsCanceled == true
),它在等待时传播一个OperationCanceledException
。 Doing it with an async method is trivial:使用异步方法执行此操作很简单:
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
I decided to switch to a non- async implementation, and now I have trouble reproducing the same behavior.我决定切换到非异步实现,但现在我无法重现相同的行为。 Wrapping a Task.FromCanceled
results correctly to a canceled ValueTask<T>
, but the type of the exception is TaskCanceledException
, which is not desirable:将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)
Another unsuccessful attempt is to wrap a Task.FromException
.另一个不成功的尝试是包装Task.FromException
。 This one propagates the correct exception type, but the task is faulted instead of canceled:这个传播了正确的异常类型,但任务是错误的而不是取消的:
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
Is there any solution to this problem, or I should accept that my API will behave inconsistently, and sometimes will propagate TaskCanceledException
s (when the token is already canceled), and other times will propagate OperationCanceledException
s (when the token is canceled later)?这个问题有什么解决方案吗,或者我应该接受我的 API 的行为不一致,有时会传播TaskCanceledException
s(当令牌已经被取消时),而其他时候会传播OperationCanceledException
s(当令牌稍后被取消时)?
Try it on Fiddle .在 Fiddle 上试试。
Update: As a practical example of the inconsistency I am trying to avoid, here is one from the built-in Channel<T>
class:更新:作为我试图避免的不一致的一个实际示例,这是来自内置Channel<T>
class 的一个:
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
The first ValueTask<int>
throws a TaskCanceledException
, because the token is already canceled.第一个ValueTask<int>
抛出TaskCanceledException
,因为令牌已被取消。 The second ValueTask<int>
throws an OperationCanceledException
, because the token is canceled 100 msec later.第二个ValueTask<int>
抛出OperationCanceledException
,因为令牌在 100 毫秒后被取消。
The best approach here is probably to simply have an async
helper method you can defer to, here;这里最好的方法可能是简单地有一个你可以遵循的async
辅助方法,这里; ie: IE:
ValueTask<int> MyMethod3(CancellationToken token)
{
if (token.IsCancellationRequested) return Cancelled(token);
// ... the rest of your non-async code here
static async ValueTask<int> Cancelled(CancellationToken token)
{
token.ThrowIfCancellationRequested();
// some dummy await, or just suppress the compiler warning about no await
await Task.Yield(); // should never be reached
return 0; // should never be reached
}
}
There is a third option that might work, but it is a lot more complicated and doesn't avoid allocation ( IValueTaskSource<TResult>
- noting that you'd still need somewhere to stash the relevant token)还有第三个选项可能有效,但它要复杂得多并且不能避免分配( IValueTaskSource<TResult>
- 注意您仍然需要在某个地方存放相关令牌)
Sneakier version:运动鞋版本:
#pragma warning disable CS1998
static async ValueTask<int> Cancelled(CancellationToken token)
#pragma warning restore CS1998
{
token.ThrowIfCancellationRequested();
return 0; // should never be reached
}
Although it throws a TaskCanceledException
as well, the ValueTask.FromCanceled method has been introduced with .NET 5 and above, which may be relevant here.虽然它也会抛出TaskCanceledException
,但ValueTask.FromCanceled方法已在 .NET 5 及更高版本中引入,这可能与此处相关。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.