将一个 CancellationToken 与异步方法的 Task 相关联

3
问题:是否有办法将CancellationToken与从async方法返回的任务(Task)相关联?
通常情况下,如果使用与任务(Task)的CancellationToken匹配的CancellationToken抛出OperationCancelledException,则任务(Task)将最终处于Canceled状态。如果它们不匹配,则任务(Task)进入Faulted状态。
void WrongCancellationTokenCausesFault()
{
    var cts1 = new CancellationTokenSource();
    var cts2 = new CancellationTokenSource();
    cts2.Cancel();

    // This task will end up in the Faulted state due to the task's CancellationToken
    // not matching the thrown OperationCanceledException's token.
    var task = Task.Run(() => cts2.Token.ThrowIfCancellationRequested(), cts1.Token);  
}

使用 async/await,我还没有找到一种方法来设置方法的 TaskCancellationToken(从而实现相同类型的功能)。从我的测试中,似乎 任何 OperationCancelledException 都会导致 async 方法进入取消状态:

async Task AsyncMethodWithCancellation(CancellationToken ct)
{
    // If ct is cancelled, this will cause the returned Task to be in the Cancelled state
    ct.ThrowIfCancellationRequested(); 
    await Task.Delay(1);

    // This will cause the returned Task to be in the Cancelled state
    var newCts = new CancellationTokenSource();
    newCts.Cancel();
    newCts.Token.ThrowIfCancellationRequested();
}

如果我从async方法中调用的某个方法被取消了(而且我并没有预期会取消,也就是说它不是这个TaskCancellationToken),那么我希望这个任务进入Faulted状态,而不是Canceled状态。因此,希望能够有更多的控制。

2个回答

2
我认为这种设计适用于普通情况:如果任何子操作被取消,取消操作会传播到父操作(最常见的情况是父操作和子操作共享取消令牌)。
如果您想要不同的语义,可以在您的异步方法中捕获OperationCanceledException并抛出符合您需要的语义的异常。如果您想要重复使用这些语义,那么一个针对Task的扩展方法应该可以胜任。

我同意 - 对于常见情况它运行良好。有点奇怪的是,一个子Task(非async)可能会因为使用错误的令牌(OperationCancelledException)而失败,但这会导致父(asyncTask处于取消状态。主要是我只是好奇并担心出现错误的情况:意外的取消不会导致我的async代码失败,也许会延迟我发现错误。 - Matt Smith
有一些关于取消的边角情况;例如,如果你将一个已经取消的令牌传递给 Task.Run,你会得到一个被取消的任务,在 await 时会引发 TaskCanceledException,但是如果你在任务开始后取消传递给 Task.Run 的令牌,你会得到一个被取消的任务,在 await 时会引发 OperationCanceledException不是 TaskCanceledException)。无论如何,你不必担心错误,因为你最终应该 await 所有任务(你关心的),任何被取消的任务在 await 时都会抛出 OperationCanceledException - Stephen Cleary

0
这里有一个Run方法,试图模仿Task.Run方法的行为,当提供一个CancellationToken参数时。由Run返回的任务只能在异步方法返回一个Canceled任务,并且相关的CancellationToken与提供的参数匹配时才能变为Canceled
/// <summary>
/// Invokes an async method (a method implemented with the async keyword), and
/// returns a proxy of the produced async task. In case the async task completes
/// in a Canceled state but the causative CancellationToken is not equal with the
/// cancellationToken argument, the proxy transitions to a Faulted state.
/// In all other cases, the proxy propagates the status of the async task as is.
/// </summary>
public static Task<TResult> Run<TResult>(Func<Task<TResult>> asyncMethod,
    CancellationToken cancellationToken)
{
    return asyncMethod().ContinueWith(t =>
    {
        if (t.IsCanceled)
        {
            try { t.GetAwaiter().GetResult(); }
            catch (Exception ex)
            {
                // In case the async method is canceled with an unknown token, throw
                // the exception. The continuation will complete in a Faulted state.
                if (ex is OperationCanceledException oce &&
                    oce.CancellationToken != cancellationToken) throw;
            }
        }

        return t; // In any other case, propagate the task as is.
    }, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default)
    .Unwrap();
}

使用示例:

Task<int> task = Run(async () =>
{
    await Task.Delay(1000, new CancellationToken(true));
    return 13;
}, new CancellationToken(false));

try { task.Wait(); } catch { }

Console.WriteLine(task.Status);

输出:

Faulted

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接