Эквивалент ContinueWith (делегат, CancellationToken) с ожиданием продолжения

У меня такая ситуация:

private Task LongRunningTask = /* Something */;

private void DoSomethingMore(Task previousTask) { }

public Task IndependentlyCancelableSuccessorTask(CancellationToken cancellationToken)
{
    return LongRunningTask.ContinueWith(DoSomethingMore, cancellationToken);
}

В частности, поведение, которое меня интересует, подробно описано в странице MSDN о задачах продолжения в следующих выражениях:

Продолжение переходит в состояние Canceled в следующих сценариях:

  • [...]
  • Когда продолжение было передано System.Threading.CancellationToken в качестве аргумента и свойство IsCancellationRequested токен true перед продолжением. В этом случае продолжение не начинается и переходит в состояние Canceled.

Выполняется код выше. Тем не менее, я нахожусь в процессе преобразования как можно большего числа моих продолжений в использование ключевого слова await.

Есть ли эквивалент с помощью await, который позволит продолжить отмену до завершения ожидаемой задачи?

Ответ 1

Следующее должно сделать это, хотя это выглядит немного неудобно:

private Task LongRunningTask = /* Something */;

private void DoSomethingMore() { }

public async Task IndependentlyCancelableSuccessorTask(
    CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    var tcs = new TaskCompletionSource<bool>();
    using (cancellationToken.Register(() => tcs.TrySetCanceled()))
        await Task.WhenAny(LongRunningTask, tcs.Task);

    cancellationToken.ThrowIfCancellationRequested();
    DoSomethingMore();
}

[UPDATE] Следуя подсказке, здесь он сформирован как помощник, основанный на Stephen Toub Реализация Затем с Await pattern:

public static class TaskExt
{
    /// <summary>
    /// Use: await LongRunningTask.Then(DoSomethingMore, cancellationToken)
    /// </summary>
    public static async Task Then(
        this Task antecedent, Action continuation, CancellationToken token)
    {
        await antecedent.When(token);
        continuation();
    }

    /// <summary>
    /// Use: await LongRunningTask.When(cancellationToken)
    /// </summary>
    public static async Task When(
        this Task antecedent, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();

        var tcs = new TaskCompletionSource<Empty>();
        using (token.Register(() => tcs.TrySetCanceled()))
            await Task.WhenAny(antecedent, tcs.Task);

        token.ThrowIfCancellationRequested();
    }

    struct Empty { };
}

Возможно, первый ThrowIfCancellationRequested() является избыточным, но я не полностью рассмотрел все случаи краев.

Ответ 2

Хотя этот ответ концептуально то же, что и у Noseratio, меня не удовлетворяет несколько деталей реализации, и поэтому публикую предлагаемую вами реализацию помощника, чтобы ее могли прокомментировать другие люди по этому вопросу.

public static async Task<TResult> WhenNotCanceled<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
{
    if (!cancellationToken.CanBeCanceled) {
        return await mainTask.ConfigureAwait(false);
    }

    cancellationToken.ThrowIfCancellationRequested();

    Task<TResult> completedTask;

    var cancellationTaskSource = new TaskCompletionSource<TResult>();
    using (cancellationToken.Register(() => cancellationTaskSource.TrySetCanceled(), useSynchronizationContext: false)
        completedTask = await Task.WhenAny(mainTask, cancellationTaskSource.Task).ConfigureAwait(false);

    cancellationToken.ThrowIfCancellationRequested();
    return await completedTask.ConfigureAwait(false);
}

public static async Task WhenNotCanceled(this Task mainTask, CancellationToken cancellationToken)
{
    if (!cancellationToken.CanBeCanceled) {
        await mainTask.ConfigureAwait(false);
        return;
    }

    cancellationToken.ThrowIfCancellationRequested();

    Task completedTask;

    var cancellationTaskSource = new TaskCompletionSource<object>();
    using (cancellationToken.Register(() => cancellationTaskSource.TrySetCanceled(), useSynchronizationContext: false)
        completedTask = await Task.WhenAny(mainTask, cancellationTaskSource.Task).ConfigureAwait(false);

    cancellationToken.ThrowIfCancellationRequested();
    await completedTask.ConfigureAwait(false);
}

Асинхронный шаблон без отмены:

public async Task IndependentlyCancelableSuccessorTask()
{
    await LongRunningTask;
    DoSomethingMore();
}

Асинхронный шаблон с отменой и WhenNotCanceled:

public async Task IndependentlyCancelableSuccessorTask(CancellationToken cancellationToken)
{
    await LongRunningTask.WhenNotCanceled(cancellationToken);
    DoSomethingMore();
}

Ответ 3

Мой ответ немного отличается от ответа @Jean Hominal и включает в себя подход @Noseratio:

public static class TaskExtensionMethods
{
    public static Task<TResult> OrWhenCancelled<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
    {
        if (!cancellationToken.CanBeCanceled)
            return mainTask;

        return OrWhenCancelled_(mainTask, cancellationToken);
    }

    private static async Task<TResult> OrWhenCancelled_<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
    {
        Task cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken);
        await Task.WhenAny(mainTask, cancellationTask).ConfigureAwait(false);

        cancellationToken.ThrowIfCancellationRequested();
        return await mainTask;
    }

    public static Task OrWhenCancelled(this Task mainTask, CancellationToken cancellationToken)
    {
        if (!cancellationToken.CanBeCanceled)
            return mainTask;

        return OrWhenCancelled_(mainTask, cancellationToken);
    }

    private static async Task OrWhenCancelled_(this Task mainTask, CancellationToken cancellationToken)
    {
        Task cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken);
        await Task.WhenAny(mainTask, cancellationTask).ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();
        await mainTask;
    }
}

Обсуждение:

  • Все решения (включая этот) не правильно обрабатывают случай, когда в исходном ContinueWith указан a TaskScheduler. В частности, рассмотрим TaskScheduler, созданный TaskScheduler.FromCurrentSynchronizationContext для использования в сценариях пользовательского интерфейса. В этом случае с исходным подходом ContinueWith вам гарантировалось, что маркер отмены был проверен до запуска делегата, но после того, как он уже попал в Основной поток (см. этот ответ). То есть старый подход имеет приятный эффект от проверки токена отмены "в последний раз" в основном потоке, прежде чем рассматривать результат задачи (т.е. Превзойден ли завершение или сбой главной задачи). Это означает, что в дополнение к использованию этих методов расширения новый код должен обернуть его await в try/finally, чтобы выполнить свою окончательную проверку CancellationToken:(. См. this вопрос.

  • @Решение Noseratio может обрабатывать вышеупомянутую проблему (если необходимо), но имеет недостаток в требовании, чтобы продолжение было помещено в делегат. На мой взгляд, это побеждает одно из больших преимуществ преобразования в использование await: код не заканчивается делегатом, он сразу после await и читается как обычный последовательный код.

Примечания:

  • Хотел бы я указать, что пустая лямбда никогда не работает (т.е. вместо того, чтобы работать только при отмене), но метод .ContinueWith этого не позволяет. Итак, я (в основном произвольно выбрал OnlyOnCancelled)

Ответ 4

Этот ответ исходит от @Servy от этого ответа (с изменениями):

public static Task WithCancellation(this Task task,
CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}

public static Task<T> WithCancellation<T>(this Task<T> task,
CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}