Partager via


Annulation dans les threads managés

À compter de .NET Framework 4, .NET utilise un modèle unifié pour l’annulation coopérative d’opérations synchrones asynchrones ou longues. Ce modèle est basé sur un objet léger appelé jeton d’annulation. Objet qui appelle une ou plusieurs opérations annulables, par exemple en créant de nouveaux threads ou tâches, transmet le jeton à chaque opération. Les opérations individuelles peuvent à leur tour transmettre des copies du jeton à d’autres opérations. À un moment donné, l’objet qui a créé le jeton peut l’utiliser pour demander que les opérations arrêtent ce qu’elles font. Seul l’objet demandeur peut émettre la demande d’annulation, et chaque écouteur est responsable de la notication de la demande et de la réponse à celle-ci 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 CancellationTokenSource objet, qui gère et envoie une notification d’annulation aux jetons d’annulation individuels.

  • Transmettez le jeton retourné par la CancellationTokenSource.Token propriété à 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 CancellationTokenSource.Cancel méthode pour fournir une notification d’annulation.

Important

La classe CancellationTokenSource implémente l’interface IDisposable. Vous devez être sûr d’appeler la CancellationTokenSource.Dispose méthode lorsque vous avez terminé d’utiliser la source du jeton d’annulation pour libérer toutes les ressources non managées qu’elle contient.

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

CancellationTokenSource et les jetons d’annulation

Le modèle d’annulation coopérative facilite la création d’applications et de bibliothèques prenant en charge les applications et bibliothèques prenant en charge les fonctionnalités suivantes :

  • L’annulation est coopérative et n’est pas forcée sur l’écouteur. L’écouteur détermine comment se terminer correctement en réponse à une demande d’annulation.

  • La demande est distincte de l’écoute. Un objet qui appelle une opération annulable peut contrôler quand (le cas échéant) l’annulation est demandée.

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

  • Un écouteur peut écouter plusieurs jetons simultanément en les joignant à un seul jeton lié.

  • Le code utilisateur peut remarquer et répondre aux demandes d’annulation à partir du code de bibliothèque, et le code de la bibliothèque peut remarquer et répondre aux demandes d’annulation à partir du code utilisateur.

  • Les écouteurs peuvent être avertis des demandes d’annulation en interrogeant, en rappelant l’inscription ou en attendant les handles d’attente.

Types d’annulation

L’infrastructure d’annulation est implémentée en tant qu’ensemble de types connexes, répertoriés dans le tableau suivant.

Nom de type Descriptif
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 de valeur légère passé à un ou plusieurs écouteurs, généralement en tant que paramètre de méthode. Les écouteurs surveillent la valeur de la IsCancellationRequested propriété du jeton en interrogeant, rappel ou handle d’attente.
OperationCanceledException Les surcharges du constructeur de cette exception acceptent un CancellationToken paramètre. Les écouteurs peuvent éventuellement lever cette exception pour vérifier la source de l’annulation et informer d’autres utilisateurs qu’ils ont 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.TaskSystem.Threading.Tasks.Task<TResult> et System.Linq.ParallelEnumerable. Nous vous recommandons d’utiliser ce modèle d’annulation coopérative pour tous les nouveaux codes de bibliothèque et d’application.

Exemple de code

Dans l’exemple suivant, l’objet demandeur crée un CancellationTokenSource objet, puis transmet sa Token propriété à l’opération annulable. L’opération qui reçoit la requête surveille la valeur de la IsCancellationRequested propriété du jeton en interrogeant. Lorsque la valeur devient true, l’écouteur peut se terminer de quelque manière que ce soit. Dans cet exemple, la méthode se ferme, ce qui est tout ce qui est nécessaire dans de nombreux cas.

Remarque

L’exemple utilise la QueueUserWorkItem méthode pour dé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 , consultez 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 Example1
    Public Sub Main1()
        ' 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 d’un nettoyage requis. Un jeton d’annulation doit faire référence à une « opération annulable », mais cette opération peut être implémentée dans votre programme. Une fois la IsCancellationRequested propriété du jeton définie truesur , elle ne peut pas être réinitialisée .false Par conséquent, les jetons d’annulation ne peuvent pas être réutilisés une fois qu’ils ont été annulés.

Si vous avez besoin d’un mécanisme d’annulation d’objet, vous pouvez le baser sur le mécanisme d’annulation d’opération en appelant la CancellationToken.Register méthode, comme illustré 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 {id} Cancel callback");
        // 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 ExampleOb1
    Public Sub MainOb1()
        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, transmettez un jeton distinct comme entrée à chaque opération annulable distincte. Ainsi, une opération peut être annulée sans affecter les autres.

Écoute et réponse aux demandes d’annulation

Dans le délégué de l’utilisateur, l’implémenteur d’une opération annulable détermine comment arrêter l’opération en réponse à une demande d’annulation. Dans de nombreux cas, le délégué utilisateur peut simplement effectuer n’importe quel nettoyage requis, puis retourner immédiatement.

Toutefois, dans des cas plus complexes, il peut être nécessaire que le délégué de l’utilisateur informe le code de la bibliothèque que l’annulation s’est produite. Dans ce cas, le bon moyen d’arrêter l’opération est que le délégué appelle la ThrowIfCancellationRequestedméthode , ce qui entraîne la levée d’un OperationCanceledException élément. Le code de la bibliothèque peut intercepter cette exception sur le thread délégué de l’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 Task classe gère OperationCanceledException de cette façon. Pour plus d’informations, consultez Annulation de tâche.

Écoute par interrogation

Pour les calculs de longue durée qui bouclent ou récursent, vous pouvez écouter une demande d’annulation en interrogeant régulièrement la valeur de la CancellationToken.IsCancellationRequested propriété. Si sa valeur est true, la méthode doit nettoyer et se terminer aussi rapidement que possible. La fréquence optimale de l’interrogation dépend du 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’a pas d’impact significatif sur les performances. L’exemple suivant montre une méthode possible 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 row As Integer = 0 To rect.rows - 1
            ' Simulating work.
            Thread.SpinWait(5000)
            Console.Write("0',1' ", col, row)
        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, consultez Guide pratique pour écouter les demandes d’annulation par interrogation.

Écoute en inscrivant un rappel

Certaines opérations peuvent être bloquées de telle sorte qu’elles ne peuvent pas vérifier la valeur du jeton d’annulation en temps voulu. Dans ce cas, vous pouvez inscrire une méthode de rappel qui débloque la méthode lorsqu’une demande d’annulation est reçue.

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

using System;
using System.Net.Http;
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)
    {
        var client = new HttpClient();

        token.Register(() =>
        {
            client.CancelPendingRequests();
            Console.WriteLine("Request cancelled!");
        });

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

Class Example4
    Private Shared Sub Main4()
        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 client As New HttpClient()

        token.Register(Sub()
                           client.CancelPendingRequests()
                           Console.WriteLine("Request cancelled!")
                       End Sub)

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

L’objet CancellationTokenRegistration gère la synchronisation des threads et garantit que le rappel cesse d’s’exécuter à un moment précis dans le temps.

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

  • La méthode de rappel doit être rapide, car elle est appelée de façon synchrone et par conséquent, l’appel à Cancel ne retourne pas tant que le rappel n’est pas retourné.

  • Si vous appelez Dispose pendant que le rappel est en cours d’exécution et que vous maintenez un verrou sur lequel le rappel est en attente, votre programme peut bloquer. Une fois Dispose retourné, vous pouvez libérer toutes les ressources requises par le rappel.

  • Les rappels ne doivent pas effectuer de thread manuel ni SynchronizationContext d’utilisation dans un rappel. Si un rappel doit s’exécuter sur un thread particulier, utilisez le System.Threading.CancellationTokenRegistration constructeur qui vous permet de spécifier que le syncContext cible est actif SynchronizationContext.Current. L’exécution de threads manuels dans un rappel peut entraîner un blocage.

Pour obtenir un exemple plus complet, consultez Guide pratique pour inscrire des rappels pour les demandes d’annulation.

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

Lorsqu’une opération pouvant être annulée peut bloquer pendant qu’elle attend sur une primitive de synchronisation telle qu’une System.Threading.ManualResetEvent ou System.Threading.Semaphore, vous pouvez utiliser la CancellationToken.WaitHandle propriété pour permettre à l’opération d’attendre à la fois sur l’événement et la demande d’annulation. Le handle d’attente du jeton d’annulation est signalé en réponse à une demande d’annulation, et la méthode peut utiliser la valeur de retour de la WaitAny méthode pour déterminer s’il s’agissait du jeton d’annulation signalé. L’opération peut ensuite simplement quitter, ou lever un 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 Wait méthodes. Vous pouvez passer la CancellationToken méthode, et lorsque l’annulation est demandée, l’événement se réveille et lève un 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, consultez Guide pratique pour écouter les demandes d’annulation qui ont 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 transmis en externe en tant qu’argument à un paramètre de méthode. Pour ce faire, créez une source de jeton lié qui peut joindre deux jetons ou plus à un 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 la source de jeton lié lorsque vous l’avez terminé. Pour obtenir un exemple plus complet, consultez Guide pratique pour écouter plusieurs demandes d’annulation.

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

L’infrastructure d’annulation unifiée permet au code de la bibliothèque d’annuler le code utilisateur, et pour que le code utilisateur annule le code de la bibliothèque de manière coopérative. La coopération fluide dépend de chaque côté en suivant ces lignes directrices :

  • 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 afin que le code utilisateur puisse demander l’annulation.

  • Si le code de la bibliothèque appelle du code utilisateur, le code de la bibliothèque doit interpréter une opération OperationCanceledException(externalToken) comme une annulation coopérative, et pas nécessairement comme une exception d’échec.

  • Les délégués utilisateur 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 instructions. Pour plus d’informations, consultez Annulation des tâches et Procédure : Annuler une requête PLINQ.

Voir aussi