使用超时处理多个 CancellationToken

Dealing with multiple CancellationTokens with timeouts

我对如何为以下情况实施取消令牌感到有些困惑。

假设我有一个方法有一个取消令牌,没有像这样指定超时。

public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
    try
    {
        Task.Delay(1000, cancellationToken)
    }
    catch (OperationCanceledException canceledException)
    {
        // Do something with canceledException
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
        throw;
    }
    catch (Exception exception)
    {
        // Do something with exception
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
        throw;
    }
}

但是在那个方法中我想调用另一个需要 CancellationToken 的方法,除了这次我想设置一个超时,就像这样。

public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
    try
    {
        var innerCancellationTokenSource = new CancellationTokenSource();
        innerCancellationTokenSource.CancelAfter(1000);
        var innerCancellationToken = innerCancellationTokenSource.Token;

        await DoSomeElseAsyncThingAsync(innerCancellationToken);
    }
    catch (OperationCanceledException canceledException)
    {
        // Do something with canceledException
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
        throw;
    }
    catch (Exception exception)
    {
        // Do something with exception
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
        throw;
    }
}

如何让 innerCancellationToken 遵守来自 cancellationToken 参数的取消请求?

我能想到的最好的是这样的:

public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
    try
    {
        await Task.WhenAny(
            DoSomeElseAsyncThingAsync(cancellationToken),
            KaboomAsync(100, cancellationToken)
        );
    }
    catch (OperationCanceledException canceledException)
    {
        // Do something with canceledException
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
        throw;
    }
    catch (Exception exception)
    {
        // Do something with exception
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
        throw;
    }
}

public static async Task KaboomAsync(int delay, CancellationToken cancellationToken = default)
{
    await Task.Delay(delay, cancellationToken);
    throw new OperationCanceledException();
}

但这并不完全正确; KaboomAsync() 函数总是会崩溃,这条路看起来很崎岖。有更好的模式吗?


Post 回答我创建了这个静态 Util 方法来节省我将样板文件放入一百万次的时间。

希望对某人有用。

public static async Task<T> CancellableUnitOfWorkHelper<T>(
    Func<CancellationToken, Task<T>> unitOfWordFunc,
    int timeOut,
    CancellationToken cancellationToken = default
)
{
    try
    {
        var innerCancellationTokenSource = new CancellationTokenSource(timeOut);
        using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
            return await unitOfWordFunc(linkedTokenSource.Token);
    }
    catch (OperationCanceledException canceledException)
    {
        Console.WriteLine(
            cancellationToken.IsCancellationRequested
                ? "Manual or parent Timeout {0}"
                : "UnitOfWork Timeout {0}"
            , canceledException
        );

        throw;
    }
    catch (Exception exception)
    {
        Console.WriteLine("Exception {0}", exception);
        throw;
    }
}

public static async Task CancellableUnitOfWorkHelper(
    Func<CancellationToken, Task> unitOfWordFunc,
    int timeOut,
    CancellationToken cancellationToken = default
)
{
    try
    {
        var innerCancellationTokenSource = new CancellationTokenSource(timeOut);
        using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
            await unitOfWordFunc(linkedTokenSource.Token);
    }
    catch (OperationCanceledException canceledException)
    {
        Console.WriteLine(
            cancellationToken.IsCancellationRequested
                ? "Manual or parent Timeout {0}"
                : "UnitOfWork Timeout {0}"
            , canceledException
        );

        throw;
    }
    catch (Exception exception)
    {
        Console.WriteLine("Exception {0}", exception);
        throw;
    }
}

它们可以这样使用。

await Util.CancellableUnitOfWorkHelper(
   token => Task.Delay(1000, token),
   200
);

await Util.CancellableUnitOfWorkHelper(
   token => Task.Delay(1000, token),
   200,
   someExistingToken
);

在这两个示例中,它将在 200 毫秒后超时,但第二个示例也将遵守来自 "someExistingToken" 令牌的手动取消或超时。

CancellationTokenSource有专门针对这个场景的方法:CreateLinkedTokenSource

在您的示例中,它可能看起来像这样:

public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
    try
    {
        var innerCancellationTokenSource = new CancellationTokenSource();

        using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
        {
            innerCancellationTokenSource.CancelAfter(1000);

            await DoSomeElseAsyncThingAsync(linkedTokenSource.Token);
        }
    }
    catch (OperationCanceledException canceledException)
    {
        // Do something with canceledException
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
        throw;
    }
    catch (Exception exception)
    {
        // Do something with exception
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
        throw;
    }
}

请注意,处置链接源很重要,否则来自父令牌源的引用将阻止它被垃圾收集。

另见 and When to dispose CancellationTokenSource?