2

我正在编写一个具有ValueTask<T>返回类型并接受CancellationToken. 如果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

我决定切换到非异步实现,但现在我无法重现相同的行为。Task.FromCanceled将结果正确包装到 cancelled 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(当令牌稍后被取消时)?

在 Fiddle 上试试


更新:作为我试图避免的不一致的一个实际示例,这是内置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>抛出 a TaskCanceledException,因为令牌已经被取消。第二个ValueTask<int>抛出一个OperationCanceledException,因为令牌在 100 毫秒后被取消。

在 Fiddle 上试试

4

1 回答 1

3

这里最好的方法可能是简单地一个async你可以遵循的辅助方法,在这里;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
    }
}

还有第三选项可能有效,但它要复杂得多并且不能避免分配(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
}
于 2021-09-02T11:11:58.100 回答