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); } } }
3 Réponses :
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); } }
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.
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?
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.
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.
D'abord, vous n'avez pas besoin de
Register
là-bas - il a un but différent. Deuxièmement, appelerCancel
directement à partir de la tâche et traiterCancellationTokenSource
n'est pas considéré comme une bonne pratique - votre première annulation ne fonctionne que par coïncidence, car toutes les exceptions dans leRegister
sont propagés à la méthodeCancel
.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
ouTaskCanceledException
) pour imprimer ces messages. Dans le gestionnaire d'exceptions générique, il est préférable d'imprimerException.ToString ()
pour voir le tout, pas seulement leException.Message
.