如何 return 传播 OperationCanceledException 的取消的 ValueTask<T>,没有 async/await?
How to return a canceled ValueTask<T> that propagates an OperationCanceledException, without async/await?
我正在写一个 API,等待时有一个 ValueTask<T>
return type, and accepts a CancellationToken
. 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
。使用异步方法完成它是微不足道的:
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 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
,这是不可取的:
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 会表现不一致,有时会传播 TaskCanceledException
s(当令牌已经被取消时),而其他时候将传播 OperationCanceledException
s(当令牌稍后被取消时)?
更新: 作为我试图避免的不一致的实际例子,这里有一个来自内置 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
第一个 ValueTask<int>
抛出一个 TaskCanceledException
,因为令牌已经被取消。第二个 ValueTask<int>
抛出 OperationCanceledException
,因为令牌在 100 毫秒后被取消。
这里最好的方法可能是简单地 拥有 一个 async
辅助方法,您可以在这里使用;即:
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
}
}
是第三个可能可行的选项,但它要复杂得多并且不会避免分配(IValueTaskSource<TResult>
- 注意你仍然需要在某个地方存放相关令牌)
运动鞋版本:
#pragma warning disable CS1998
static async ValueTask<int> Cancelled(CancellationToken token)
#pragma warning restore CS1998
{
token.ThrowIfCancellationRequested();
return 0; // should never be reached
}
我正在写一个 API,等待时有一个 ValueTask<T>
return type, and accepts a CancellationToken
. 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
。使用异步方法完成它是微不足道的:
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 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
,这是不可取的:
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 会表现不一致,有时会传播 TaskCanceledException
s(当令牌已经被取消时),而其他时候将传播 OperationCanceledException
s(当令牌稍后被取消时)?
更新: 作为我试图避免的不一致的实际例子,这里有一个来自内置 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
第一个 ValueTask<int>
抛出一个 TaskCanceledException
,因为令牌已经被取消。第二个 ValueTask<int>
抛出 OperationCanceledException
,因为令牌在 100 毫秒后被取消。
这里最好的方法可能是简单地 拥有 一个 async
辅助方法,您可以在这里使用;即:
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
}
}
是第三个可能可行的选项,但它要复杂得多并且不会避免分配(IValueTaskSource<TResult>
- 注意你仍然需要在某个地方存放相关令牌)
运动鞋版本:
#pragma warning disable CS1998
static async ValueTask<int> Cancelled(CancellationToken token)
#pragma warning restore CS1998
{
token.ThrowIfCancellationRequested();
return 0; // should never be reached
}