Эквивалент ContinueWith (делегат, CancellationToken) с ожиданием продолжения
У меня такая ситуация:
private Task LongRunningTask = /* Something */;
private void DoSomethingMore(Task previousTask) { }
public Task IndependentlyCancelableSuccessorTask(CancellationToken cancellationToken)
{
return LongRunningTask.ContinueWith(DoSomethingMore, cancellationToken);
}
В частности, поведение, которое меня интересует, подробно описано в странице MSDN о задачах продолжения в следующих выражениях:
Продолжение переходит в состояние 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);
}