3
votes

Annulation de plusieurs tâches en enregistrant des rappels sur des jetons d'annulation

J'ai le morceau de code suivant avec la sortie ci-dessous. Je m'attendais à ce que la deuxième tâche soit annulée car elle enregistre également un rappel sur le jeton d'annulation. Mais l'annulation ne se produit que sur la première tâche, où l'annulation d'origine a été effectuée. Les annulations ne sont-elles pas censées être propagées à toutes les instances de jetons? L ' article de Microsoft sur les jetons d'annulation n'explique pas bien cela.

Y a-t-il des indications pour expliquer pourquoi cela se produit?

Code:

This is task one
Write something
Task 1 exception: Thread was being aborted.
Task 1 was cancelled
This is task two
Thread was being aborted.
Both tasks over

Résultat:

class Program
    {
        static void Main(string[] args)
        {
            AsyncProgramming();
            Console.ReadLine();
        }

        private static async void AsyncProgramming()
        {

            try
            {
                using (var cts = new CancellationTokenSource())
                {
                    var task2 = CreateTask2(cts);
                    var task1 = CreateTask1(cts);

                    Thread.Sleep(5000);
                    await Task.WhenAll(task2, task1);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            Console.WriteLine("Both tasks over");
        }

        private static async Task CreateTask1(CancellationTokenSource cts)
        {
            try
            {
                cts.Token.Register(() => { cts.Token.ThrowIfCancellationRequested(); });
                await Task.Delay(5000);
                Console.WriteLine("This is task one");
                cts.Cancel();
                Console.WriteLine("This should not be printed because the task was cancelled");
            }
            catch (Exception e)
            {
                Console.WriteLine("Task 1 exception: " + e.Message);
                Console.WriteLine("Task 1 was cancelled");
            }

        }

        private static async Task CreateTask2(CancellationTokenSource cts)
        {
            try
            {
                cts.Token.Register(() =>
                {
                    Console.WriteLine("Write something");
                    Thread.CurrentThread.Abort();
                    cts.Token.ThrowIfCancellationRequested();
                });
                await Task.Delay(8000);

                Console.WriteLine("This is task two");
            }
            catch (Exception e)
            {
                Console.WriteLine("Task 2 was cancelled by Task 1");
                Console.WriteLine(e);
            }
        }
    }


2 commentaires

D'abord, vous n'avez pas besoin de Register là-bas - il a un but différent. Deuxièmement, appeler Cancel directement à partir de la tâche et traiter CancellationTokenSource n'est pas considéré comme une bonne pratique - votre première annulation ne fonctionne que par coïncidence, car toutes les exceptions dans le Register sont propagés à la méthode Cancel .


Pourquoi appelez-vous Thread.CurrentThread.Abort () ? Cela gâche complètement votre démo. Les tâches ne sont pas annulées, vous abandonnez simplement le thread principal de votre application! Les messages d'erreur sont également trompeurs. N'imprimez pas de messages comme «La tâche 1 a été annulée» dans un gestionnaire d'exceptions générique. Attrapez des exceptions spécifiques ( OperationCanceledException ou TaskCanceledException ) pour imprimer ces messages. Dans le gestionnaire d'exceptions générique, il est préférable d'imprimer Exception.ToString () pour voir le tout, pas seulement le Exception.Message .


3 Réponses :


2
votes

Ce n'est pas seulement la deuxième tâche qui ne s'annule pas. Les deux enregistrements au jeton fonctionnent et les deux ThrowIfCancellationRequested se déclenchent, mais ils ne sont pas traités car ils s'exécutent dans un thread différent.

Cela se produit en arrière-plan (deux fois):

Une exception de type 'System.OperationCanceledException' s'est produite dans mscorlib.dll mais n'a pas été gérée dans le code utilisateur

Ce que vous devez faire est d'appeler cts.Token.ThrowIfCancellationRequested (); dans votre fonction au lieu de vous inscrire à l'événement.

Voir les exemples sur https://docs.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads

En ce moment, vous combinez deux méthodes d'annulation: l'inscription à l'événement d'annulation de jeton ( Token.Register ), et à lancer si le jeton est annulé ( Token.ThrowIfCancellationRequested ).

Soit vous vous abonnez à l'événement d'annulation et exécutez votre propre logique d'annulation / nettoyage, soit vous enregistrez votre code de fonction si vous devez annuler votre opération.

Un exemple ressemblerait à ceci :

private static async Task CreateTask2(CancellationToken token)
{
    try
    {
        // Pass on the token when calling other functions.
        await Task.Delay(8000, token);

        // And manually check during long operations.
        for (int i = 0; i < 10000; i++)
        {
            // Do we need to cancel?
            token.ThrowIfCancellationRequested();

            // Simulating work.
            Thread.SpinWait(5000);
        }

        Console.WriteLine("This is task two");
    }
    catch (Exception e)
    {
        Console.WriteLine("Task 2 was cancelled by Task 1");
        Console.WriteLine(e);
    }
}


9 commentaires

Cela semble fonctionner, mais je ne comprends pas ce que vous entendez par «attente de la tâche». Pourquoi l'exécution ne reprend-elle pas après le délai?


C'est le même article que j'ai utilisé pour référence mais il n'y a rien à ce sujet.


Vous mélangez l'annulation d'objet et d'opération. Dans votre tâche, appelez simplement le Token.ThrowIfCancellationRequested (); sans vous inscrire.


Comment l'ajout de await Task.Delay (8000, cts.Token) vous a-t-il aidé?


Le passage du jeton au Delay fait que le retard s'annule lui-même lorsque le jeton se déclenche. Parce que votre annulation se déclenche après 5000 ms (à partir de la tâche 1), cela se produit alors que la tâche 2 attend toujours son retard de 8000 ms qui s'annule ensuite.


Mais sans passer le jeton à Delay dans la tâche 2, la tâche 2 s'exécutait sans aucune annulation. Dans l'o / p, "Ceci est la tâche deux" était imprimé.


Oui, passer le jeton au Task.Delay a fait que le délai écoute correctement le jeton et donc s'annule. C'était le délai de lever l'exception d'annulation, pas votre gestionnaire Register () . J'ai ajouté un exemple de la façon dont vous vérifieriez l'annulation à la place.


continuons cette discussion dans le chat .


Pour le moment, vous combinez deux méthodes d'annulation: l'inscription à l'événement d'annulation de jeton , Register n'est pas un moyen d'annulation, c'est juste une notification, rien de plus.



0
votes

L'enregistrement d'un délégué par Register est juste un moyen de notifier lorsqu'un token passe à l'état annulé, pas plus. Pour effectuer l'annulation, vous devez réagir à cette notification dans le code et c'est surtout nécessaire lorsque l'exécution que vous souhaitez annuler passe à une étape où le jeton d'annulation n'est pas vérifié (par exemple, parce qu'une méthode en cours d'exécution n'accepte tout simplement pas CancellationToken en tant que paramètre) mais vous avez toujours besoin d'un certain contrôle de l'état d'annulation. Mais dans tous les cas, lorsque vous traitez avec l'exécuon de code qui a accès à CancellationToken , vous n'avez tout simplement pas besoin de vous abonner à la notification d'annulation.

Dans votre cas, le premier délégué lève une exception et cette exception est propagée à l'appel Cancel seulement c'est pourquoi la tâche est annulée, mais c'est une conception incorrecte car vous ne devriez pas traiter avec CancellationTokenSource dans vos tâches et ne devrait pas initier une annulation là-dedans, donc je dirais que la première annulation ne fonctionne que par coïncidence. Pour la deuxième tâche, le délégué est appelé mais rien ne déclenche l'annulation à l'intérieur de la tâche, alors pourquoi devrait-elle être annulée?


0 commentaires

2
votes

La première chose est que lorsque vous appelez CancellationToken.Register , tout ce qu'il fait normalement est de stocker le délégué à appeler plus tard.

Le flux de thread / logique appelant CancellationTokenSource.Cancel exécute tous les délégués précédemment enregistrés, quel que soit l'endroit d'où ils ont été enregistrés. Cela signifie que toute exception lancée dans ceux-ci ne se rapporte en aucun cas aux méthodes qui ont appelé Register .

Note latérale 1: J'ai dit normalement ci-dessus, car il y a un cas où l'appel à Register exécutera le délégué tout de suite. Je pense que c'est pourquoi la documentation msdn est encore plus déroutante. Plus précisément: si le jeton a déjà été annulé, Register exécutera le délégué immédiatement, au lieu de le stocker pour une exécution ultérieure. En dessous, cela se passe dans CancellationTokenSource.InternalRegister .

La deuxième chose pour compléter le tableau est que tout ce que fait CancellationToken.ThrowIfCancellationRequested est de lever une exception d'où qu'elle soit exécutée. Ce serait normalement là où CancellationTokenSource.Cancel a été appelé. Notez que normalement tous les délégués enregistrés sont exécutés, même si certains d'entre eux lancent une exception.

Note d'accompagnement 2: lancer ThreadAbortException modifie la logique voulue dans la méthode Cancel , car cette exception spéciale ne peut pas être interceptée. Face à cela, annuler arrête d'exécuter d'autres délégués. Il en va de même pour le code d'appel, même lors de la détection d'exceptions.

La dernière chose à noter, c'est que la présence du CancellationToken n'affecte pas le flux logique des méthodes. Toutes les lignes de la méthode s'exécutent, sauf si du code quitte explicitement la méthode, par exemple en lançant une exception. C'est ce qui se passe si vous transmettez le jeton d'annulation aux appels Task.Delay et qu'il est annulé ailleurs avant que le temps ne s'écoule. C'est aussi ce qui se passe si vous appelez CancellationToken.ThrowIfCancellationRequested après des lignes spécifiques dans votre méthode.


1 commentaires

Ainsi, les deux rappels sont enregistrés et exécutés dans le contexte de la requête / thread qui annule le jeton en appelant cts.Cancel. Puisque le rappel n'est jamais exécuté dans le contexte de la deuxième tâche, l'exception n'est jamais levée dans la deuxième tâche et elle s'exécute normalement. Les deux rappels sont exécutés dans le contexte de la première tâche, par conséquent la première tâche ne se termine jamais et lève une exception.