Как отменить задачу в ожидании?

164

Я играю с этими задачами Windows 8 WinRT и пытаюсь отменить задачу, используя метод, описанный ниже, и в какой-то момент это работает. Метод CancelNotification ДОЛЖЕН вызываться, что заставляет вас думать, что задача была отменена, но в фоновом режиме задача продолжает выполняться, затем после ее завершения состояние задачи всегда завершается и никогда не отменяется. Есть ли способ полностью остановить задачу, когда она отменена?

private async void TryTask()
{
    CancellationTokenSource source = new CancellationTokenSource();
    source.Token.Register(CancelNotification);
    source.CancelAfter(TimeSpan.FromSeconds(1));
    var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token);

    await task;            

    if (task.IsCompleted)
    {
        MessageDialog md = new MessageDialog(task.Result.ToString());
        await md.ShowAsync();
    }
    else
    {
        MessageDialog md = new MessageDialog("Uncompleted");
        await md.ShowAsync();
    }
}

private int slowFunc(int a, int b)
{
    string someString = string.Empty;
    for (int i = 0; i < 200000; i++)
    {
        someString += "a";
    }

    return a + b;
}

private void CancelNotification()
{
}
Карло
источник
Только что нашел эту статью, которая помогла мне понять различные способы отменить.
Уве Кейм

Ответы:

239

Читайте об отмене (которая была введена в .NET 4.0 и с тех пор практически не изменилась) и асинхронном шаблоне на основе задач , который содержит рекомендации по использованию CancellationTokenс asyncметодами.

Подводя итог, вы передаете CancellationTokenв каждый метод, который поддерживает отмену, и этот метод должен периодически проверять его.

private async Task TryTask()
{
  CancellationTokenSource source = new CancellationTokenSource();
  source.CancelAfter(TimeSpan.FromSeconds(1));
  Task<int> task = Task.Run(() => slowFunc(1, 2, source.Token), source.Token);

  // (A canceled task will raise an exception when awaited).
  await task;
}

private int slowFunc(int a, int b, CancellationToken cancellationToken)
{
  string someString = string.Empty;
  for (int i = 0; i < 200000; i++)
  {
    someString += "a";
    if (i % 1000 == 0)
      cancellationToken.ThrowIfCancellationRequested();
  }

  return a + b;
}
Стивен Клири
источник
2
Вау, отличная информация! Это сработало отлично, теперь мне нужно выяснить, как обрабатывать исключение в асинхронном методе. Спасибо чувак! Я прочитаю то, что вы предложили.
Карло
8
Нет. Большинство долгосрочных синхронных методов имеют какой-то способ их отмены - иногда путем закрытия базового ресурса или вызова другого метода. CancellationTokenимеет все возможности, необходимые для взаимодействия с пользовательскими системами отмены, но ничто не может отменить невозможный метод.
Стивен Клири
1
Ах я вижу. Таким образом, лучший способ отловить исключение ProcessCancelledException - заключить 'await' в try / catch? Иногда я получаю AggregatedException и не могу с этим справиться.
Карло
3
Правильно. Я рекомендую вам никогда не использовать Waitили Resultв asyncметодах; Вы должны всегда использовать awaitвместо этого, который правильно разворачивает исключение.
Стивен Клири
11
Просто любопытно, есть ли причина, по которой ни один из примеров не использует CancellationToken.IsCancellationRequestedи вместо этого предлагает исключение?
Джеймс М
41

Или, чтобы избежать изменений slowFunc(например, у вас нет доступа к исходному коду):

var source = new CancellationTokenSource(); //original code
source.Token.Register(CancelNotification); //original code
source.CancelAfter(TimeSpan.FromSeconds(1)); //original code
var completionSource = new TaskCompletionSource<object>(); //New code
source.Token.Register(() => completionSource.TrySetCanceled()); //New code
var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token); //original code

//original code: await task;  
await Task.WhenAny(task, completionSource.Task); //New code

Вы также можете использовать хорошие методы расширения из https://github.com/StephenCleary/AsyncEx, и они будут выглядеть так просто:

await Task.WhenAny(task, source.Token.AsTask());
sonatique
источник
1
Это выглядит очень сложно ... как реализация асинхронного ожидания. Я не думаю, что такие конструкции делают исходный код более читабельным.
Максим
1
Спасибо, одно замечание - регистрационный токен должен быть позже утилизирован, второе - используйте, ConfigureAwaitиначе вы можете пострадать в приложениях пользовательского интерфейса.
astrowalker
@astrowalker: да, действительно, регистрация токена должна быть отменена (удалена). Это можно сделать внутри делегата, который передается в Register (), вызывая dispose для объекта, который возвращается Register (). Однако, поскольку токен «источника» в данном случае является только локальным, все будет очищено в любом случае ...
sonatique
1
На самом деле все, что нужно, это вложить его using.
astrowalker
@astrowalker ;-) да, на самом деле ты прав. В этом случае это гораздо более простое решение! Однако, если вы хотите вернуть Task.WhenAny напрямую (без ожидания), вам нужно что-то еще. Я говорю это потому, что однажды столкнулся с проблемой рефакторинга: прежде чем я использовал ... подождите. Затем я удалил await (и асинхронную функцию), поскольку она была единственной, не заметив, что я полностью нарушил код. Полученную ошибку было сложно найти. Поэтому я не хочу использовать using () вместе с async / await. Я чувствую, что шаблон Dispose не подходит для асинхронных
операций в
15

Один случай, который не был рассмотрен, это как обрабатывать отмену внутри асинхронного метода. Возьмем, к примеру, простой случай, когда вам нужно загрузить некоторые данные в сервис, заставить их вычислить что-то, а затем вернуть некоторые результаты.

public async Task<Results> ProcessDataAsync(MyData data)
{
    var client = await GetClientAsync();
    await client.UploadDataAsync(data);
    await client.CalculateAsync();
    return await client.GetResultsAsync();
}

Если вы хотите поддержать отмену, тогда самым простым способом будет передать токен и проверить, был ли он отменен между каждым вызовом асинхронного метода (или с использованием ContinueWith). Если они очень длинные, хотя вы можете подождать, чтобы отменить их. Я создал небольшой вспомогательный метод, который вместо этого завершился неудачей, как только его отменили.

public static class TaskExtensions
{
    public static async Task<T> WaitOrCancel<T>(this Task<T> task, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        await Task.WhenAny(task, token.WhenCanceled());
        token.ThrowIfCancellationRequested();

        return await task;
    }

    public static Task WhenCanceled(this CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
        return tcs.Task;
    }
}

Поэтому, чтобы использовать его, просто добавьте .WaitOrCancel(token)к любому асинхронному вызову:

public async Task<Results> ProcessDataAsync(MyData data, CancellationToken token)
{
    Client client;
    try
    {
        client = await GetClientAsync().WaitOrCancel(token);
        await client.UploadDataAsync(data).WaitOrCancel(token);
        await client.CalculateAsync().WaitOrCancel(token);
        return await client.GetResultsAsync().WaitOrCancel(token);
    }
    catch (OperationCanceledException)
    {
        if (client != null)
            await client.CancelAsync();
        throw;
    }
}

Обратите внимание, что это не остановит задачу, которую вы ждали, и продолжит работу. Вам нужно использовать другой механизм , чтобы остановить его, например, CancelAsyncвызов в примере, или еще лучше пройти в то же самое CancellationTokenк Taskтак , что он может обрабатывать отмены в конце концов. Попытка прервать поток не рекомендуется .

kjbartel
источник
1
Обратите внимание, что хотя это отменяет ожидание задачи, оно не отменяет фактическую задачу (например, UploadDataAsyncможет продолжаться в фоновом режиме, но после ее завершения вызов не будет выполнен, CalculateAsyncпоскольку эта часть уже перестала ждать). Это может быть или не быть проблематичным для вас, особенно если вы хотите повторить операцию. Прохождение CancellationTokenполностью вниз является предпочтительным вариантом, когда это возможно.
Мирал
1
@Miral, что верно, однако есть много асинхронных методов, которые не принимают токены отмены. Возьмем, к примеру, службы WCF, которые при создании клиента с помощью асинхронных методов не будут включать токены отмены. На самом деле, как показывает пример, и, как заметил Стивен Клири, предполагается, что длительные синхронные задачи могут как-то их отменить.
kjbartel
1
Вот почему я сказал «когда это возможно». В основном я просто хотел, чтобы это предупреждение было упомянуто, чтобы люди, находящие этот ответ позже, не получили неправильное впечатление.
Мирал
@ Мирал Спасибо. Я обновил, чтобы отразить это предостережение.
kjbartel
К сожалению, это не работает с такими методами, как 'NetworkStream.WriteAsync'.
Zeokat
6

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

Comments.AsAsyncAction().Completed += new AsyncActionCompletedHandler(CommentLoadComplete);

Где обработчик событий выглядит так

private void CommentLoadComplete(IAsyncAction sender, AsyncStatus status )
{
    if (status == AsyncStatus.Canceled)
    {
        return;
    }
    CommentsItemsControl.ItemsSource = Comments.Result;
    CommentScrollViewer.ScrollToVerticalOffset(0);
    CommentScrollViewer.Visibility = Visibility.Visible;
    CommentProgressRing.Visibility = Visibility.Collapsed;
}

С этим маршрутом вся обработка уже сделана для вас, когда задача отменяется, она просто запускает обработчик события, и вы можете видеть, была ли она отменена там.

Smeegs
источник