Як скасувати завдання в очікуванні?


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
Нічого чудова інформація! Це працювало чудово, тепер мені потрібно розібратися, як обробити виняток у методі async. Спасибі людино! Я прочитаю запропоновані вами речі.
Карло

8
Ні. Більшість тривалих синхронних методів мають певний спосіб їх скасувати - іноді закриваючи базовий ресурс або викликаючи інший метод. CancellationTokenмає всі гачки, необхідні для взаємодії з користувацькими системами скасування, але ніщо не може скасувати неприйнятний метод.
Стівен Клірі

1
А, бачу. Тож найкращий спосіб зловити ProcessCanceiledException - це загортання "очікування" у спробу / ловити? Іноді я отримую 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());

1
Це виглядає дуже хитро ... як ціла асинхроніка - чекайте реалізації. Я не думаю, що такі конструкції роблять вихідний код більш читабельним.
Максим

1
Дякую, одна примітка - реєстрація жетону повинна бути згодом утилізована, друга річ - використовуйте ConfigureAwaitінакше, ви можете поранитися в додатках інтерфейсу.
astrowalker

@astrowalker: так, дійсно, реєстрація токена краще не буде зареєстрована (розпоряджена). Це можна зробити всередині делегата, який передається в Register (), зателефонувавши dispose на об'єкт, який повертається Register (). Однак оскільки маркер "джерела" в цьому випадку лише локальний, все одно буде очищено ...
sonatique

1
Насправді все, що потрібно, це гніздо using.
astrowalker

@astrowalker ;-) так, ти прав. У цьому випадку це набагато простіше рішення! Однак якщо ви хочете повернути Task.WhenAny безпосередньо (не чекаючи), тоді вам потрібно щось інше. Я говорю це тому, що колись я потрапив на проблему рефакторингу так: перед тим, як я скористався ... чекай. Потім я видалив функцію очікування (і асинхронізацію функції), оскільки вона була єдиною, не помічаючи, що я повністю порушував код. Отриманого помилку було важко знайти. Тому я неохоче користуюся використанням () разом з async / wait. Я відчуваю, що утилізація все одно не відповідає асинхронним речам ...
sonatique

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він міг обробляти скасування в підсумку. Намагатися перервати нитку не рекомендується .


1
Зауважте, що поки це скасовує очікування завдання, воно не скасовує фактичне завдання (наприклад, UploadDataAsyncможе продовжуватися у фоновому режимі, але після завершення він не буде робити дзвінок, CalculateAsyncтому що ця частина вже перестала чекати). Це може бути або не бути проблематичним для вас, особливо якщо ви хочете повторити операцію. CancellationTokenКоли це можливо, проходження всього шляху вниз - це кращий варіант.
Miral

1
@Miral це правда, проте існує багато методів асинхронізації, які не приймають жетони скасування. Візьмемо для прикладу послуги WCF, які при створенні клієнта методами Async не включатимуть маркери скасування. Дійсно, як показує приклад, і як зазначив Стівен Клірі, передбачається, що тривалі синхронні завдання мають певний спосіб їх скасувати.
kjbartel

1
Тому я сказав "коли можливо". В основному я просто хотів згадати цей застереження, щоб люди, які знайшли цю відповідь пізніше, не склали неправильного враження.
Miral

@Miral Дякую Я оновив, щоб відобразити цей застереження.
kjbartel

На жаль, це не працює з такими методами, як "NetworkStream.WriteAsync".
Zeokat

6

Я просто хочу додати до вже прийнятої відповіді. Я застряг у цьому, але я йшов іншим маршрутом, щоб вирішити повну подію. Замість того, щоб працювати в очікуванні, я додаю завершений обробник до завдання.

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;
}

З цим маршрутом все оброблення вже зроблено для вас, коли завдання скасовано, воно просто запускає обробник подій, і ви можете побачити, чи було його скасовано там.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.