Condition de concurrence avec CancellationToken où CancellationTokenSource n’est annulé que sur le fil principal

Considérons une application Winforms, où nous avons un bouton qui génère des résultats. Si l’utilisateur appuie une seconde fois sur le bouton, il doit annuler la première demande pour générer des résultats et en démarrer une nouvelle.

Nous utilisons le modèle ci-dessous, mais nous ne soaps pas si une partie du code est nécessaire pour éviter une condition de concurrence (voir les lignes commentées).

private CancellationTokenSource m_cts; private void generateResultsButton_Click(object sender, EventArgs e) { // Cancel the current generation of results if necessary if (m_cts != null) m_cts.Cancel(); m_cts = new CancellationTokenSource(); CancellationToken ct = m_cts.Token; // **Edit** Clearing out the label m_label.Text = Ssortingng.Empty; // **Edit** Task task = Task.Run(() => { // Code here to generate results. return 0; }, ct); task.ContinueWith(t => { // Is this code necessary to prevent a race condition? // if (ct.IsCancellationRequested) // return; int result = t.Result; m_label.Text = result.ToSsortingng(); }, ct, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext()); } 

Remarquer:

  • Nous n’annulons jamais CancellationTokenSource sur le fil principal.
  • Nous utilisons le même CancellationToken dans la suite que dans la tâche initiale.

Nous nous demandons si la séquence d’événements suivante est possible ou non:

  1. L’utilisateur clique sur le bouton “générer des résultats”. La tâche initiale t1 est lancée.
  2. L’utilisateur clique à nouveau sur le bouton “générer les résultats”. Un message Windows est envoyé en queue, mais le gestionnaire n’a pas encore été exécuté.
  3. La tâche t1 se termine.
  4. TPL commence se prépare à commencer la suite (puisque le CancellationToken n’est pas encore annulé). Le planificateur de tâches poste le travail dans la file de messages Windows (pour l’exécuter sur le thread principal).
  5. GenerateResultsButton_Click pour le 2e clic commence à s’exécuter et le CancellationTokenSource est annulé.
  6. Le travail sur les continuations commence et fonctionne comme si le jeton n’avait pas été annulé (c’est-à-dire qu’il affiche ses résultats dans l’interface utilisateur).

Donc, je pense que la question se résume à:

Lorsque le travail est posté sur le thread principal (à l’aide de TaskScheduler.FromCurrentSynchronizationContext() ), le TPL vérifie-t-il CancellationToken sur le thread principal avant d’exécuter l’action de la tâche, ou puis poster le travail sur le SynchronizationContext ?

    En supposant que je lise la question correctement, vous êtes inquiet à propos de la séquence d’événements suivante:

    1. Le bouton est cliqué, la tâche T0 est planifiée sur le pool de threads, la suite C0 est planifiée comme une continuation de T0 et doit être exécutée sur le planificateur de tâches du contexte de synchronisation.
    2. Le bouton est cliqué à nouveau. Supposons que la pompe de messages est occupée à faire autre chose. La file de messages se compose désormais d’un élément, le gestionnaire de clics.
    3. T0 termine, C0 est envoyé dans la file de messages. La queue contient maintenant deux éléments, le gestionnaire de clics et l’exécution de C0 .
    4. Le message du gestionnaire de clics est pompé et le gestionnaire signale le jeton à l’origine de l’annulation de T0 et de C0 . Ensuite, il planifie T1 sur le pool de threads et C1 en continu de la même manière qu’à l’étape 1 .
    5. Le message ‘execute C0 ‘ est toujours dans la queue, il est donc traité maintenant. Est-ce qu’il exécute la continuation que vous aviez l’intention d’annuler?

    La réponse est non. TryExecuteTask n’exécutera pas une tâche dont l’annulation a été signalée. C’est impliqué par cette documentation, mais explicitement énoncé sur la page TaskStatus , qui spécifie

    Annulé – La tâche a accusé réception de l’annulation en lançant une OperationCanceledException avec son propre AnnulationToken alors que le jeton était dans l’état signalé ou que AnnulationToken de la tâche était déjà signalé avant que la tâche ne commence à s’exécuter .

    Donc, à la fin de la journée, T0 sera dans l’état RanToCompletion et C0 sera dans l’état Canceled .

    C’est tout, bien sûr, en supposant que le SynchronizationContext ne permet pas l’exécution simultanée de tâches (comme vous le savez, le Windows Forms ne le fait pas – je remarque simplement que ce n’est pas une exigence des contextes de synchronisation).

    En outre, il convient de noter que la réponse exacte à votre dernière question, à savoir si le jeton d’annulation est vérifié dans le contexte de la demande d’annulation ou du moment où la tâche est exécutée, correspond réellement aux deux . En plus de la vérification finale dans TryExecuteTask , dès que l’annulation est demandée, la structure appellera TryDequeue , opération facultative que les planificateurs de tâches peuvent prendre en charge. Le planificateur de contexte de synchronisation ne le prend pas en charge. Mais si cela se produisait, la différence pourrait être que le message ‘exécuter C0 ‘ serait entièrement extrait de la file de messages du thread et qu’il ne tenterait même pas d’exécuter la tâche.

    À mon avis, quel que soit le thread qui vérifie CencellationToken, vous devez prendre en compte la possibilité que votre continuation puisse être planifiée et que l’utilisateur puisse annuler la demande pendant l’exécution de la continuation. Donc, je pense que le contrôle qui a été commenté devrait être vérifié et devrait probablement être vérifié à nouveau après avoir lu le résultat:

      task.ContinueWith(t => { // Is this code necessary to prevent a race condition? if (ct.IsCancellationRequested) return; int result = t.Result; if (ct.IsCancellationRequested) return; m_label.Text = result.ToSsortingng(); }, ct, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext()); 

    J’appendais également une continuité pour traiter la condition d’annulation séparément:

      task.ContinueWith(t => { // Do whatever is appropriate here. }, ct, TaskContinuationOptions.OnlyOnCanceled, TaskScheduler.FromCurrentSynchronizationContext()); 

    De cette façon, vous avez toutes les possibilités couvertes.