Annulation dans les threads managés

.NET Framework 4 et versions ultérieures utilisent un modèle unifié pour l’annulation coopérative des opérations asynchrones ou des opérations synchrones de longue durée. Ce modèle est basé sur un objet léger appelé jeton d'annulation. L'objet qui appelle une ou plusieurs opérations annulables, par exemple en créant de nouveaux threads ou de nouvelles tâches, passe le jeton à chaque opération. Chaque opération peut, à son tour, passer des copies du jeton à d'autres opérations. Ultérieurement, l'objet qui a créé le jeton peut l'utiliser pour demander que les opérations arrêtent leur action. Seul l'objet demandeur peut émettre la demande d'annulation. Chaque écouteur est chargé d'accepter la demande et d'y répondre de manière appropriée et en temps voulu.

Le modèle général d’implémentation du modèle d’annulation coopérative est le suivant :

  • Instanciez un objet CancellationTokenSource qui gère et envoie une notification d'annulation pour chaque jeton d'annulation.

  • Passez le jeton retourné par la propriété CancellationTokenSource.Token à chaque tâche ou thread qui écoute l'annulation.

  • Fournissez un mécanisme pour chaque tâche ou thread pour répondre à l’annulation.

  • Appelez la méthode CancellationTokenSource.Cancel pour fournir une notification d'annulation.

Important

La classe CancellationTokenSource implémente l’interface IDisposable. Quand vous aurez terminé d'utiliser la source du jeton d'annulation, vous devrez appeler la méthode CancellationTokenSource.Dispose pour libérer les ressources non managées qu'elle contient.

L'illustration suivante montre la relation entre une source de jeton et toutes les copies de ce jeton.

CancellationTokenSource and cancellation tokens

Le modèle d’annulation coopérative facilite la création d’applications et de bibliothèques prenant en charge l’annulation. De plus, il prend en charge les fonctionnalités suivantes :

  • L'annulation est coopérative et n'est pas imposée à l'écouteur. L'écouteur choisit comment s'arrêter correctement en réponse à une demande d'annulation.

  • La demande est différente de l'écoute. Un objet qui appelle une opération annulable peut contrôler à quel moment l'annulation est demandée (le cas échéant).

  • L'objet demandeur émet la demande d'annulation vers toutes les copies du jeton à l'aide d'un seul appel de méthode.

  • Un écouteur peut écouter plusieurs jetons simultanément en les rassemblant sous la forme d'un même jeton lié.

  • Le code utilisateur peut remarquer et répondre aux demandes d'annulation à partir du code de bibliothèque, et ce dernier peut remarquer et répondre aux demandes d'annulation à partir du code utilisateur.

  • Les écouteurs peuvent être avertis des demandes d'annulation par le biais d'une interrogation, d'une inscription de rappel ou bien en attendant des handles d'attente.

Types d'annulation

L'infrastructure d'annulation est implémentée comme un ensemble de types connexes, qui sont répertoriés dans le tableau suivant.

Nom de type Description
CancellationTokenSource Objet qui crée un jeton d'annulation et émet également la demande d'annulation pour toutes les copies de ce jeton.
CancellationToken Type valeur léger passé à un ou plusieurs écouteurs, généralement sous la forme d'un paramètre de méthode. Les écouteurs surveillent la valeur de la propriété IsCancellationRequested du jeton par le biais d'interrogations, de rappels ou de handles d'attente.
OperationCanceledException Les surcharges du constructeur de cette exception acceptent CancellationToken comme paramètre. Les écouteurs peuvent éventuellement lever cette exception pour vérifier la source de l'annulation et informer les autres qu'elle a répondu à une demande d'annulation.

Le modèle d’annulation est intégré à .NET dans plusieurs types. Les plus importants sont System.Threading.Tasks.Parallel, System.Threading.Tasks.Task,System.Threading.Tasks.Task<TResult> et System.Linq.ParallelEnumerable. Nous vous recommandons d’utiliser ce modèle d’annulation coopérative pour tout nouveau code de bibliothèque et d’application.

Exemple de code

Dans l'exemple suivant, l'objet demandeur crée un objet CancellationTokenSource, puis passe sa propriété Token à l'opération annulable. L'opération qui reçoit la demande surveille la valeur de la propriété IsCancellationRequested du jeton par le biais d'une interrogation. Quand la valeur devient true, l'écouteur peut s'arrêter de quelque manière appropriée que ce soit. Dans cet exemple, la méthode s'arrête, ce qui suffit dans de nombreux cas.

Notes

L’exemple utilise la méthode QueueUserWorkItem pour montrer que l’infrastructure d’annulation coopérative est compatible avec les API héritées. Pour obtenir un exemple qui utilise le type préféré System.Threading.Tasks.Task, voir Comment : annuler une tâche et ses enfants.

using System;
using System.Threading;

public class Example
{
    public static void Main()
    {
        // Create the token source.
        CancellationTokenSource cts = new CancellationTokenSource();

        // Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
        Thread.Sleep(2500);

        // Request cancellation.
        cts.Cancel();
        Console.WriteLine("Cancellation set in token source...");
        Thread.Sleep(2500);
        // Cancellation should have happened, so call Dispose.
        cts.Dispose();
    }

    // Thread 2: The listener
    static void DoSomeWork(object? obj)
    {
        if (obj is null)
            return;

        CancellationToken token = (CancellationToken)obj;

        for (int i = 0; i < 100000; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1);
                // Perform cleanup if necessary.
                //...
                // Terminate the operation.
                break;
            }
            // Simulate some work.
            Thread.SpinWait(500000);
        }
    }
}
// The example displays output like the following:
//       Cancellation set in token source...
//       In iteration 1430, cancellation has been requested...
Imports System.Threading

Module Example
    Public Sub Main()
        ' Create the token source.
        Dim cts As New CancellationTokenSource()

        ' Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
        Thread.Sleep(2500)

        ' Request cancellation by setting a flag on the token.
        cts.Cancel()
        Console.WriteLine("Cancellation set in token source...")
        Thread.Sleep(2500)
        ' Cancellation should have happened, so call Dispose.
        cts.Dispose()
    End Sub

    ' Thread 2: The listener
    Sub DoSomeWork(ByVal obj As Object)
        Dim token As CancellationToken = CType(obj, CancellationToken)

        For i As Integer = 0 To 1000000
            If token.IsCancellationRequested Then
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1)
                ' Perform cleanup if necessary.
                '...
                ' Terminate the operation.
                Exit For
            End If

            ' Simulate some work.
            Thread.SpinWait(500000)
        Next
    End Sub
End Module
' The example displays output like the following:
'       Cancellation set in token source...
'       In iteration 1430, cancellation has been requested...

Annulation d'opération et annulation d'objet

Dans l’infrastructure d’annulation coopérative, l’annulation fait référence aux opérations, et non aux objets. La demande d'annulation signifie que l'opération doit s'arrêter dès que possible après l'exécution de tout nettoyage nécessaire. Un jeton d'annulation doit faire référence à une opération annulable. Toutefois, cette opération peut être implémentée dans votre programme. Après avoir défini la propriété IsCancellationRequested du jeton sur true, celle-ci ne peut pas être réinitialisée à la valeur false. Les jetons d'annulation ne peuvent donc pas être réutilisés après avoir été annulés.

Si vous avez besoin d'un mécanisme d'annulation d'objets, vous pouvez le baser sur le mécanisme d'annulation d'opérations en appelant la méthode CancellationToken.Register, comme indiqué dans l'exemple suivant.

using System;
using System.Threading;

class CancelableObject
{
    public string id;

    public CancelableObject(string id)
    {
        this.id = id;
    }

    public void Cancel()
    {
        Console.WriteLine("Object {0} Cancel callback", id);
        // Perform object cancellation here.
    }
}

public class Example1
{
    public static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        // User defined Class with its own method for cancellation
        var obj1 = new CancelableObject("1");
        var obj2 = new CancelableObject("2");
        var obj3 = new CancelableObject("3");

        // Register the object's cancel method with the token's
        // cancellation request.
        token.Register(() => obj1.Cancel());
        token.Register(() => obj2.Cancel());
        token.Register(() => obj3.Cancel());

        // Request cancellation on the token.
        cts.Cancel();
        // Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose();
    }
}
// The example displays the following output:
//       Object 3 Cancel callback
//       Object 2 Cancel callback
//       Object 1 Cancel callback
Imports System.Threading

Class CancelableObject
    Public id As String

    Public Sub New(id As String)
        Me.id = id
    End Sub

    Public Sub Cancel()
        Console.WriteLine("Object {0} Cancel callback", id)
        ' Perform object cancellation here.
    End Sub
End Class

Module Example
    Public Sub Main()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token

        ' User defined Class with its own method for cancellation
        Dim obj1 As New CancelableObject("1")
        Dim obj2 As New CancelableObject("2")
        Dim obj3 As New CancelableObject("3")

        ' Register the object's cancel method with the token's
        ' cancellation request.
        token.Register(Sub() obj1.Cancel())
        token.Register(Sub() obj2.Cancel())
        token.Register(Sub() obj3.Cancel())

        ' Request cancellation on the token.
        cts.Cancel()
        ' Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose()
    End Sub
End Module
' The example displays output like the following:
'       Object 3 Cancel callback
'       Object 2 Cancel callback
'       Object 1 Cancel callback

Si un objet prend en charge plusieurs opérations annulables simultanées, passez un jeton en tant qu'entrée à chaque opération annulable. De cette façon, une opération peut être annulée sans affecter les autres.

Demandes d'annulation : écoute et réponse

Dans le délégué utilisateur, l'implémenteur d'une opération annulable détermine la façon de terminer l'opération en réponse à une demande d'annulation. Dans de nombreux cas, le délégué utilisateur peut simplement effectuer le nettoyage nécessaire, puis être immédiatement retourné.

Toutefois, dans des cas plus complexes, le délégué utilisateur devra notifier le code de bibliothèque qu'une annulation s'est produite. Dans ce cas, il convient de terminer l'opération en appelant le délégué de la méthode ThrowIfCancellationRequested, ce qui entraînera la levée de OperationCanceledException. Le code de bibliothèque peut intercepter cette exception sur le thread du délégué utilisateur et examiner le jeton de l'exception pour déterminer si l'exception indique une annulation coopérative ou une autre situation exceptionnelle.

La classe Task gère OperationCanceledException de cette façon. Pour plus d’informations, voir Annulation de tâches.

Écoute par interrogation

Pour les calculs de longue durée qui effectuent des boucles récursives ou non, vous pouvez écouter une demande d'annulation en interrogeant régulièrement la valeur de la propriété CancellationToken.IsCancellationRequested. Si sa valeur est de true, la méthode doit effectuer un nettoyage et se terminer aussi rapidement que possible. La fréquence d'interrogation optimale varie selon le type d'application. Il incombe au développeur de déterminer la meilleure fréquence d'interrogation pour un programme donné. L'interrogation elle-même n'altère pas beaucoup les performances. L'exemple suivant montre une méthode d'interrogation.

static void NestedLoops(Rectangle rect, CancellationToken token)
{
   for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) {
      // Assume that we know that the inner loop is very fast.
      // Therefore, polling once per column in the outer loop condition
      // is sufficient.
      for (int row = 0; row < rect.rows; row++) {
         // Simulating work.
         Thread.SpinWait(5_000);
         Console.Write("{0},{1} ", col, row);
      }
   }

   if (token.IsCancellationRequested) {
      // Cleanup or undo here if necessary...
      Console.WriteLine("\r\nOperation canceled");
      Console.WriteLine("Press any key to exit.");

      // If using Task:
      // token.ThrowIfCancellationRequested();
   }
}
Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)
    Dim col As Integer
    For col = 0 To rect.columns - 1
        ' Assume that we know that the inner loop is very fast.
        ' Therefore, polling once per column in the outer loop condition
        ' is sufficient.
        For col As Integer = 0 To rect.rows - 1
            ' Simulating work.
            Thread.SpinWait(5000)
            Console.Write("0',1' ", x, y)
        Next
    Next

    If token.IsCancellationRequested = True Then
        ' Cleanup or undo here if necessary...
        Console.WriteLine(vbCrLf + "Operation canceled")
        Console.WriteLine("Press any key to exit.")

        ' If using Task:
        ' token.ThrowIfCancellationRequested()
    End If
End Sub

Pour obtenir un exemple plus complet, voir Comment : écouter les demandes d'annulation par l'interrogation.

Écoute par inscription de rappel

Certaines opérations peuvent être bloquées et ne plus pouvoir vérifier la valeur des jetons d'annulation en temps voulu. Dans ce cas, vous pouvez inscrire une méthode de rappel qui débloque la méthode quand une demande d'annulation est reçue.

La méthode Register retourne un objet CancellationTokenRegistration utilisé spécialement à cet effet. L'exemple suivant montre comment utiliser la méthode Register pour annuler une requête web asynchrone.

using System;
using System.Net;
using System.Threading;

class Example4
{
    static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        StartWebRequest(cts.Token);

        // cancellation will cause the web
        // request to be cancelled
        cts.Cancel();
    }

    static void StartWebRequest(CancellationToken token)
    {
        WebClient wc = new WebClient();
        wc.DownloadStringCompleted += (s, e) => Console.WriteLine("Request completed.");

        // Cancellation on the token will
        // call CancelAsync on the WebClient.
        token.Register(() =>
        {
            wc.CancelAsync();
            Console.WriteLine("Request cancelled!");
        });

        Console.WriteLine("Starting request.");
        wc.DownloadStringAsync(new Uri("http://www.contoso.com"));
    }
}
Imports System.Net
Imports System.Threading

Class Example
    Private Shared Sub Main()
        Dim cts As New CancellationTokenSource()

        StartWebRequest(cts.Token)

        ' cancellation will cause the web 
        ' request to be cancelled
        cts.Cancel()
    End Sub

    Private Shared Sub StartWebRequest(token As CancellationToken)
        Dim wc As New WebClient()
        wc.DownloadStringCompleted += Function(s, e) Console.WriteLine("Request completed.")

        ' Cancellation on the token will 
        ' call CancelAsync on the WebClient.
        token.Register(Function()
                           wc.CancelAsync()
                           Console.WriteLine("Request cancelled!")

                       End Function)

        Console.WriteLine("Starting request.")
        wc.DownloadStringAsync(New Uri("http://www.contoso.com"))
    End Sub
End Class

L’objet CancellationTokenRegistration gère la synchronisation des threads et s’assure que le rappel cessera de s’exécuter à un point précis dans le temps.

Pour garantir la réactivité du système et éviter les interblocages, les instructions suivantes doivent être suivies lors de l'inscription des rappels :

  • La méthode de rappel doit être rapide, car elle est appelée de façon synchrone. L’appel à Cancel ne sera donc pas retourné avant le retour du rappel.

  • Si vous appelez Dispose pendant l'exécution du rappel et détenez un verrou que le rappel attend, votre programme peut connaître un interblocage. Après le retour de Dispose, vous pourrez libérer toutes les ressources requises par le rappel.

  • Les rappels ne doivent pas exécuter n'importe quel thread manuel ou n'importe quelle utilisation de SynchronizationContext dans un rappel. Si un rappel doit s’exécuter sur un thread particulier, utilisez le constructeur System.Threading.CancellationTokenRegistration qui permet de spécifier que la cible syncContext est le SynchronizationContext.Current actif. L'exécution de threads manuels dans un rappel peut entraîner un interblocage.

Pour obtenir un exemple plus complet, voir Comment : enregistrer des rappels pour les demandes d'annulation.

Écoute à l'aide d'un handle d'attente

Quand une opération annulable risque de se bloquer en attendant une primitive de synchronisation, telle que System.Threading.ManualResetEvent ou System.Threading.Semaphore, vous pouvez utiliser la propriété CancellationToken.WaitHandle pour que l'opération attende à la fois l'événement et la demande d'annulation. Le handle d'attente du jeton d'annulation sera signalé en réponse à une demande d'annulation et la méthode pourra utiliser la valeur de retour de la méthode WaitAny pour déterminer s'il s'agit de l'annulation du jeton à l'origine du signalement. L’opération peut alors simplement s’arrêter ou lever une OperationCanceledException, selon les besoins.

// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
       WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                          new TimeSpan(0, 0, 20));
' Wait on the event if it is not signaled.
Dim waitHandles() As WaitHandle = {mre, token.WaitHandle}
Dim eventThatSignaledIndex =
    WaitHandle.WaitAny(waitHandles, _
                       New TimeSpan(0, 0, 20))

System.Threading.ManualResetEventSlim et System.Threading.SemaphoreSlim prennent en charge l’infrastructure d’annulation dans leurs méthodes Wait. Vous pouvez passer CancellationToken à la méthode. Quand l'annulation sera demandée, l'événement se réveillera et lèvera une OperationCanceledException.

try
{
    // mres is a ManualResetEventSlim
    mres.Wait(token);
}
catch (OperationCanceledException)
{
    // Throw immediately to be responsive. The
    // alternative is to do one more item of work,
    // and throw on next iteration, because
    // IsCancellationRequested will be true.
    Console.WriteLine("The wait operation was canceled.");
    throw;
}

Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);
Try
    ' mres is a ManualResetEventSlim
    mres.Wait(token)
Catch e As OperationCanceledException
    ' Throw immediately to be responsive. The
    ' alternative is to do one more item of work,
    ' and throw on next iteration, because
    ' IsCancellationRequested will be true.
    Console.WriteLine("Canceled while waiting.")
    Throw
End Try

' Simulating work.
Console.Write("Working...")
Thread.SpinWait(500000)

Pour obtenir un exemple plus complet, voir Comment : écouter les demandes d'annulation avec des handles d'attente.

Écoute simultanée de plusieurs jetons

Dans certains cas, un écouteur peut avoir à écouter simultanément plusieurs jetons d'annulation. Par exemple, une opération annulable peut avoir à surveiller un jeton d’annulation interne en plus d’un jeton passé de manière externe comme argument à un paramètre de méthode. Pour ce faire, créez une source de jeton lié rassemblant plusieurs jetons au sein d'un même jeton, comme illustré dans l'exemple suivant.

public void DoWork(CancellationToken externalToken)
{
    // Create a new token that combines the internal and external tokens.
    this.internalToken = internalTokenSource.Token;
    this.externalToken = externalToken;

    using (CancellationTokenSource linkedCts =
            CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
    {
        try
        {
            DoWorkInternal(linkedCts.Token);
        }
        catch (OperationCanceledException)
        {
            if (internalToken.IsCancellationRequested)
            {
                Console.WriteLine("Operation timed out.");
            }
            else if (externalToken.IsCancellationRequested)
            {
                Console.WriteLine("Cancelling per user request.");
                externalToken.ThrowIfCancellationRequested();
            }
        }
    }
}
Public Sub DoWork(ByVal externalToken As CancellationToken)
    ' Create a new token that combines the internal and external tokens.
    Dim internalToken As CancellationToken = internalTokenSource.Token
    Dim linkedCts As CancellationTokenSource =
    CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)
    Using (linkedCts)
        Try
            DoWorkInternal(linkedCts.Token)
        Catch e As OperationCanceledException
            If e.CancellationToken = internalToken Then
                Console.WriteLine("Operation timed out.")
            ElseIf e.CancellationToken = externalToken Then
                Console.WriteLine("Canceled by external token.")
                externalToken.ThrowIfCancellationRequested()
            End If
        End Try
    End Using
End Sub

Notez que vous devez appeler Dispose sur la source de jeton lié quand vous en avez terminé avec lui. Pour obtenir un exemple plus complet, voir Comment : écouter plusieurs demandes d'annulation.

Coopération entre du code de bibliothèque et du code utilisateur

L'infrastructure d'annulation unifiée permet au code de bibliothèque d'annuler du code utilisateur, et au code utilisateur d'annuler du code de bibliothèque de façon coopérative. Pour une coopération harmonieuse, chaque côté doit respecter les recommandations suivantes :

  • Si le code de bibliothèque fournit des opérations annulables, il doit également fournir des méthodes publiques qui acceptent un jeton d'annulation externe pour que le code utilisateur puisse demander une annulation.

  • Si le code de bibliothèque émet un appel dans le code utilisateur, le code de bibliothèque doit interpréter un OperationCanceledException(externalToken) comme une annulation coopérative, et pas nécessairement comme une exception d'échec.

  • Les délégués utilisateurs doivent tenter de répondre aux demandes d'annulation du code de bibliothèque en temps voulu.

System.Threading.Tasks.Task et System.Linq.ParallelEnumerable sont des exemples de classes qui suivent ces recommandations. Pour plus d’informations, consultez Annulation de tâches et Procédure : annulation d’une requête PLINQ.

Voir aussi