Partager via


Annulation

Le .NET Framework version 4 introduit un nouveau modèle unifié pour l'annulation coopérative des opérations asynchrones ou synchrones à durée d'exécution longue. Ce modèle est basé sur un objet simplifié appelé jeton d'annulation. L'objet qui appelle une opération annulable, par exemple en créant un nouveau thread ou une nouvelle tâche, passe le jeton à l'opération. Cette opération peut, à son tour, passer des copies du jeton à d'autres opérations. L'objet qui a créé le jeton peut l'utiliser ultérieurement pour demander à l'opération d'arrêter son activité. Seul l'objet demandeur peut émettre la demande d'annulation, et chaque écouteur est chargé de remarquer la demande et d'y répondre en temps voulu. L'illustration suivante présente la relation entre la source d'un jeton et toutes les copies de son jeton.

CancellationTokenSource et CancellationTokens

Le nouveau modèle d'annulation simplifie la création d'applications et de bibliothèques compatibles avec l'annulation, et il prend en charge les fonctionnalités suivantes :

  • L'annulation est coopérative et n'est pas imposée à l'écouteur. L'écouteur détermine la façon de se terminer 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 le moment où (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 simultanément plusieurs jetons en les joignant dans un jeton lié.

  • Le code utilisateur peut remarquer des demandes d'annulation provenant d'un code de bibliothèque et y répondre, et le code de bibliothèque peut remarquer des demandes d'annulation provenant d'un code utilisateur et y répondre.

  • Les écouteurs peuvent être avertis de demandes d'annulation par l'interrogation, l'inscription du rappel ou l'attente sur des handles d'attente.

Nouveaux types d'annulation

La nouvelle infrastructure d'annulation est implémentée sous la forme d'un ensemble de types connexes, 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 simplifié 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 l'interrogation, le rappel ou un handle d'attente.

OperationCanceledException

Les nouvelles surcharges de cette exception acceptent un CancellationToken comme paramètre d'entrée. Les écouteurs peuvent éventuellement lever cette exception pour vérifier la source de l'annulation et avertir d'autres utilisateurs qu'elle a répondu à une demande d'annulation.

Le nouveau modèle d'annulation est intégré dans le .NET Framework 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 nouveau modèle d'annulation pour tout le 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 l'interrogation. Lorsque 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 simplement, ce qui, dans de nombreux cas, est la seule opération requise.

RemarqueRemarque

L'exemple utilise la méthode QueueUserWorkItem pour montrer que la nouvelle infrastructure d'annulation est compatible avec les API héritées.Pour obtenir un exemple qui utilise le nouveau type System.Threading.Tasks.Task par défaut, consultez Comment : annuler une tâche et ses enfants.

Shared Sub CancelWithThreadPoolMiniSnippet()


    'Thread 1: The Requestor
    ' Create the token source.
    Dim cts As New CancellationTokenSource()

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

    ' Request cancellation by setting a flag on the token.
    cts.Cancel()
    ' end block
End Sub

'Thread 2: The Listener
Shared Sub DoSomeWork(ByVal obj As Object)

    Dim token As CancellationToken = CType(obj, CancellationToken)
    For i As Integer = 0 To 1000000

        ' Simulating work.
        Thread.SpinWait(5000000)

        If token.IsCancellationRequested Then

            ' Perform cleanup if necessary.
            '...
            ' Terminate the operation.
            Exit For
        End If
    Next
End Sub
static void CancelWithThreadPoolMiniSnippet()
{

    //Thread 1: The Requestor
    // Create the token source.
    CancellationTokenSource cts = new CancellationTokenSource();

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

    // Request cancellation by setting a flag on the token.
    cts.Cancel();
}

//Thread 2: The Listener
static void DoSomeWork(object obj)
{
    CancellationToken token = (CancellationToken)obj;
    for (int i = 0; i < 100000; i++)
    {
        // Simulating work.
        Thread.SpinWait(5000000);

        if (token.IsCancellationRequested)
        {
            // Perform cleanup if necessary.
            //...
            // Terminate the operation.
            break;
        }
    }
}

Annulation d'opération et annulation d'objet

Dans la nouvelle infrastructure d'annulation, 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 requis. Un jeton d'annulation doit faire référence à une « opération annulable » ; toutefois, cette opération puisse être implémentée dans votre programme. Une fois la valeur true affectée à la propriété IsCancellationRequested du jeton, il n'est pas possible de la réinitialiser avec la valeur false. Par conséquent, les jetons d'annulation ne peuvent pas être réutilisés après avoir é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, comme indiqué dans l'exemple suivant.

Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token

' User defined Class with its own method for cancellation
Dim obj1 As New MyCancelableObject()
Dim obj2 As New MyCancelableObject()
Dim obj3 As New MyCancelableObject()

' 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()
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

// User defined Class with its own method for cancellation
var obj1 = new MyCancelableObject();
var obj2 = new MyCancelableObject();
var obj3 = new MyCancelableObject();

// 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();

Si un objet prend en charge plusieurs opérations annulables simultanées, passez un jeton séparé comme entrée à chaque opération annulable distincte. Ainsi, une opération peut être annulée sans affecter les autres.

Écoute et réponse à des demandes d'annulation

L'implémenteur d'une opération annulable détermine, dans le délégué utilisateur, la façon de mettre fin à l'opération en réponse à une demande d'annulation. Dans de nombreux cas, le délégué utilisateur peut simplement exécuter tout le nettoyage requis, puis effectuer immédiatement un retour.

Toutefois, dans des cas plus complexes, il peut s'avérer nécessaire, pour le délégué utilisateur, d'indiquer au code de bibliothèque qu'une annulation a été effectuée. Dans ces cas-là, la façon correcte de terminer l'opération consiste, pour le délégué, à appeler ThrowIfCancellationRequested(), ce qui entraînera la levée d'un OperationCanceledException. Dans le .NET Framework version 4, les nouvelles surcharges de cette exception prennent un CancellationToken comme argument. Le code de bibliothèque peut intercepter cette exception sur le thread du délégué utilisateur, et examine le jeton de l'exception afin de 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, consultez Annulation de tâches.

Écoute par l'interrogation

Pour les calculs à durée d'exécution longue qui s'exécutent en boucle ou sont parcourus de manière récursive, vous pouvez écouter une demande d'annulation en interrogeant régulièrement la valeur de la propriété CancellationToken.IsCancellationRequested. Si la valeur est true, la méthode doit procéder à un nettoyage 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'altère pas beaucoup les performances. L'exemple suivant présente une façon possible d'effectuer une interrogation.

    Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)

        For x As Integer = 0 To rect.columns

            For y As Integer = 0 To rect.rows

                ' Simulating work.
                Thread.SpinWait(5000)
                Console.Write("0' end block,1' end block ", x, y)
            Next

            ' Assume that we know that the inner loop is very fast.
            ' Therefore, checking once per row is sufficient.
            If token.IsCancellationRequested = True Then

                ' Cleanup or undo here if necessary...
                Console.WriteLine("\r\nCancelling after row 0' end block.", x)
                Console.WriteLine("Press any key to exit.")
                ' then...
                Exit For
                ' ...or, if using Task:
                ' token.ThrowIfCancellationRequested()
            End If
        Next
    End Sub

static void NestedLoops(Rectangle rect, CancellationToken token)
{
    for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++)
    {
        for (int y = 0; y < rect.rows; y++)
        {
            // Simulating work.
            Thread.SpinWait(5000);
            Console.Write("{0},{1} ", x, y);
        }

        // Assume that we know that the inner loop is very fast.
        // Therefore, checking once per row is sufficient.
        if (token.IsCancellationRequested)
        {
            // Cleanup or undo here if necessary...
            Console.WriteLine("\r\nCancelling after row {0}.", x);
            Console.WriteLine("Press any key to exit.");
            // then...
            break;
            // ...or, if using Task:
            // token.ThrowIfCancellationRequested();
        }
    }
}

Pour un exemple plus complet, consultez Comment : écouter les demandes d'annulation par l'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 ces cas-là, vous pouvez inscrire une méthode de rappel qui débloque la méthode lorsqu'une demande d'annulation est reçue.

La méthode Register retourne un objet CancellationTokenRegistration qui est spécialement utilisé à cette fin. L'exemple suivant montre comment utiliser la méthode Register pour annuler une requête Web asynchrone.

Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
Dim wc As New WebClient()

' To request cancellation on the token
' will call CancelAsync on the WebClient.
token.Register(Sub() wc.CancelAsync())

Console.WriteLine("Starting request")
wc.DownloadStringAsync(New Uri("https://www.contoso.com"))
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;            
WebClient wc = new WebClient();

// To request cancellation on the token
// will call CancelAsync on the WebClient.
token.Register(() => wc.CancelAsync());

Console.WriteLine("Starting request");
wc.DownloadStringAsync(new Uri("https://www.contoso.com"));

L'objet CancellationTokenRegistration gère la synchronisation des threads et vérifie que le rappel cessera de s'exécuter à un moment donné.

Pour vérifier la réactivité du système et éviter les interblocages, les indications suivantes doivent être suivies lors de l'enregistrement de rappels :

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

  • 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 pouvez libérer toutes les ressources requises par le rappel.

  • Les rappels ne doivent pas exécuter de thread manuel ni utiliser SynchronizationContext. Si un rappel doit s'exécuter sur un thread particulier, utilisez le constructeur System.Threading.CancellationTokenRegistration qui vous permet de spécifier que le syncContext cible est le SynchronizationContext.Current actif. L'exécution de threads manuels dans un rappel peut provoquer un interblocage.

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

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

Lorsqu'une opération annulable peut se bloquer pendant qu'elle attend sur une primitive de synchronisation telle qu'un System.Threading.ManualResetEvent ou un System.Threading.Semaphore, vous pouvez utiliser la propriété CancellationToken.WaitHandle pour permettre à l'opération d'attendre à la fois sur l'événement et sur 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 peut utiliser la valeur de retour de la méthode WaitAny pour déterminer si c'est le jeton d'annulation qui est à l'origine de la signalisation. L'opération peut alors simplement s'arrêter, ou lever un OperationCanceledException, selon le cas.

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

Dans le nouveau code qui cible le .NET Framework version 4, System.Threading.ManualResetEventSlim et System.Threading.SemaphoreSlim prennent tous les deux en charge la nouvelle infrastructure d'annulation dans leurs méthodes Wait. Vous pouvez passer CancellationToken à la méthode ; lorsque l'annulation est demandée, l'événement est alors réactivé et lève un OperationCanceledException.

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)
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);

Pour un exemple plus complet, consultez 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 cela, créez une source de jeton lié qui peut joindre deux jetons, ou plus, dans un même jeton, comme indiqué dans l'exemple suivant.

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
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();
            }
        }
    }
}

Notez que vous devez appeler Dispose sur la source du jeton lié lorsque vous n'en avez plus besoin. Pour un exemple plus complet, consultez 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 à du code de bibliothèque d'annuler du code utilisateur, et à du code utilisateur d'annuler du code de bibliothèque de façon coopérative. Une coopération harmonieuse dépend du respect, par les deux parties, des indications 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 afin que le code utilisateur puisse demander l'annulation.

  • Si le code de bibliothèque effectue 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 utilisateur doivent tenter de répondre en temps voulu aux demandes d'annulation du code de bibliothèque.

System.Threading.Tasks.Task et System.Linq.ParallelEnumerable sont des exemples de classes qui suivent ces indications. Pour plus d'informations, consultez Annulation de tâches et Comment : annuler une requête PLINQ.

Voir aussi

Autres ressources

Éléments fondamentaux du threading managé