Comment annuler une tâche en attente?

164

Je joue avec ces tâches Windows 8 WinRT et j'essaie d'annuler une tâche en utilisant la méthode ci-dessous, et cela fonctionne jusqu'à un certain point. La méthode CancelNotification est appelée, ce qui vous fait penser que la tâche a été annulée, mais en arrière-plan, la tâche continue de s'exécuter, puis une fois terminée, l'état de la tâche est toujours terminé et jamais annulé. Existe-t-il un moyen d'arrêter complètement la tâche lorsqu'elle est annulée?

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()
{
}
Carlo
la source
Je viens de trouver cet article qui m'a aidé à comprendre les différentes façons d'annuler.
Uwe Keim

Réponses:

239

Renseignez-vous sur l' annulation (qui a été introduite dans .NET 4.0 et est en grande partie inchangée depuis lors) et le modèle asynchrone basé sur les tâches , qui fournit des instructions sur l'utilisation CancellationTokendes asyncméthodes.

Pour résumer, vous passez un CancellationTokendans chaque méthode qui prend en charge l'annulation, et cette méthode doit le vérifier périodiquement.

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;
}
Stephen Cleary
la source
2
Wow super info! Cela a parfaitement fonctionné, je dois maintenant comprendre comment gérer l'exception dans la méthode async. Merci mec! Je vais lire ce que vous avez suggéré.
Carlo
8
La plupart des méthodes synchrones de longue durée ont un moyen de les annuler - parfois en fermant une ressource sous-jacente ou en appelant une autre méthode. CancellationTokena tous les hooks nécessaires pour interopérer avec les systèmes d'annulation personnalisés, mais rien ne peut annuler une méthode non annulable.
Stephen Cleary
1
Ah, je vois. Donc, la meilleure façon d'attraper le ProcessCancelledException est d'encapsuler le 'await' dans un try / catch? Parfois, j'obtiens l'exception AggregatedException et je ne peux pas gérer cela.
Carlo
3
Droite. Je vous recommande de ne jamais utiliser Waitni Resultdans les asyncméthodes; vous devriez toujours utiliser à la awaitplace, qui déballe l'exception correctement.
Stephen Cleary
11
Juste curieux, y a-t-il une raison pour laquelle aucun des exemples n'utilise CancellationToken.IsCancellationRequestedet suggère plutôt de lancer des exceptions?
James M
41

Ou, pour éviter de modifier slowFunc(disons que vous n'avez pas accès au code source par exemple):

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

Vous pouvez également utiliser de belles méthodes d'extension à partir de https://github.com/StephenCleary/AsyncEx et avoir l'air aussi simple que:

await Task.WhenAny(task, source.Token.AsTask());
sonatique
la source
1
Cela semble très délicat ... dans l'ensemble de l'implémentation async-await. Je ne pense pas que de telles constructions rendent le code source plus lisible.
Maxim
1
Merci, une note - l'enregistrement du jeton devrait être éliminé plus tard, deuxième chose - à utiliser ConfigureAwaitsinon vous pourriez être blessé dans les applications d'interface utilisateur.
astrowalker
@astrowalker: oui, en effet, l'enregistrement du token doit être désenregistré (supprimé). Cela peut être fait à l'intérieur du délégué qui est passé à Register () en appelant dispose sur l'objet qui est retourné par Register (). Cependant puisque le jeton "source" n'est que local dans ce cas, tout sera effacé de toute façon ...
sonatique
1
En fait, il suffit de l'emboîter using.
astrowalker
@astrowalker ;-) oui vous avez raison en fait. Dans ce cas, c'est la solution bien plus simple! Cependant, si vous souhaitez retourner directement Task.WhenAny (sans attendre), vous avez besoin d'autre chose. Je dis cela parce que j'ai déjà rencontré un problème de refactoring comme celui-ci: avant d'utiliser ... attendez. J'ai alors supprimé l'attente (et l'asynchrone sur la fonction) car c'était la seule, sans remarquer que je cassais complètement le code. Le bogue résultant était difficile à trouver. Je suis donc réticent à utiliser using () avec async / await. Je pense que le modèle Dispose ne va pas bien avec les choses asynchrones de toute façon ...
sonatique
15

Un cas qui n'a pas été couvert est la façon de gérer l'annulation à l'intérieur d'une méthode asynchrone. Prenons, par exemple, un cas simple où vous devez télécharger des données sur un service pour le calculer, puis renvoyer des résultats.

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

Si vous souhaitez prendre en charge l'annulation, le moyen le plus simple serait de transmettre un jeton et de vérifier s'il a été annulé entre chaque appel de méthode asynchrone (ou en utilisant ContinueWith). S'il s'agit d'appels très longs, vous pourriez attendre un certain temps avant d'annuler. J'ai créé une petite méthode d'aide pour échouer à la place dès l'annulation.

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

Donc, pour l'utiliser, ajoutez simplement .WaitOrCancel(token)à tout appel asynchrone:

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

Notez que cela n'arrêtera pas la tâche que vous attendiez et qu'elle continuera à s'exécuter. Vous aurez besoin d'utiliser un mécanisme différent pour l' arrêter, comme l' CancelAsyncappel dans l'exemple, ou mieux encore passer dans le même CancellationTokenà la Taskfaçon qu'il puisse gérer l'annulation par la suite. Il n'est pas recommandé d' essayer d'annuler le thread .

kjbartel
la source
1
Notez que bien que cela annule l'attente de la tâche, cela n'annule pas la tâche réelle (par exemple, UploadDataAsyncpeut continuer en arrière-plan, mais une fois terminé, il ne fera pas l'appel CalculateAsynccar cette partie a déjà cessé d'attendre). Cela peut être problématique ou non pour vous, surtout si vous souhaitez réessayer l'opération. Passer le CancellationTokentout en bas est l'option préférée, lorsque cela est possible.
Miral
1
@Miral c'est vrai mais il existe de nombreuses méthodes asynchrones qui ne prennent pas de jetons d'annulation. Prenez par exemple les services WCF, qui lorsque vous générez un client avec des méthodes Async n'incluent pas de jetons d'annulation. En effet, comme le montre l'exemple, et comme Stephen Cleary l'a également noté, on suppose que les tâches synchrones de longue durée ont un moyen de les annuler.
kjbartel
1
C'est pourquoi j'ai dit «quand c'est possible». La plupart du temps, je voulais juste que cette mise en garde soit mentionnée afin que les personnes qui trouvent cette réponse plus tard ne se fassent pas une mauvaise impression.
Miral
@Miral Merci. J'ai mis à jour pour refléter cette mise en garde.
kjbartel
Malheureusement, cela ne fonctionne pas avec des méthodes telles que «NetworkStream.WriteAsync».
Zeokat
6

Je veux juste ajouter à la réponse déjà acceptée. J'étais coincé là-dessus, mais j'allais sur une voie différente pour gérer l'événement complet. Plutôt que d'exécuter await, j'ajoute un gestionnaire terminé à la tâche.

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

Où le gestionnaire d'événements ressemble à ceci

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

Avec cet itinéraire, toute la gestion est déjà effectuée pour vous, lorsque la tâche est annulée, elle déclenche simplement le gestionnaire d'événements et vous pouvez voir si elle a été annulée à cet endroit.

Smeegs
la source