Share via


Annulering in beheerde threads

Vanaf .NET Framework 4 maakt .NET gebruik van een uniform model voor het opzeggen van asynchrone of langdurige synchrone bewerkingen. Dit model is gebaseerd op een lichtgewicht object dat een annuleringstoken wordt genoemd. Het object dat een of meer geannuleerde bewerkingen aanroept, bijvoorbeeld door nieuwe threads of taken te maken, geeft het token door aan elke bewerking. Afzonderlijke bewerkingen kunnen op hun beurt kopieën van het token doorgeven aan andere bewerkingen. Op een later tijdstip kan het object dat het token heeft gemaakt, het gebruiken om aan te vragen of de bewerkingen stoppen wat ze doen. Alleen het aanvragende object kan de annuleringsaanvraag uitgeven en elke listener is verantwoordelijk voor het op de juiste en tijdige wijze noteren van de aanvraag en erop te reageren.

Het algemene patroon voor het implementeren van het samenwerkingsannuleringsmodel is:

  • Instantieer een CancellationTokenSource object, dat annuleringsmeldingen beheert en verzendt naar de afzonderlijke annuleringstokens.

  • Geef het token dat door de CancellationTokenSource.Token eigenschap wordt geretourneerd door aan elke taak of thread die luistert naar annulering.

  • Geef een mechanisme op voor elke taak of thread om te reageren op annulering.

  • Roep de methode aan om een CancellationTokenSource.Cancel melding van annulering op te geven.

Belangrijk

Met de klasse CancellationTokenSource wordt de IDisposable-interface geïmplementeerd. Zorg ervoor dat u de CancellationTokenSource.Dispose methode aanroept wanneer u klaar bent met het gebruik van de annuleringstokenbron om onbeheerde resources vrij te maken die deze bevat.

In de volgende afbeelding ziet u de relatie tussen een tokenbron en alle kopieën van het token.

CancellationTokenSource and cancellation tokens

Met het samenwerkingsannuleringsmodel kunt u eenvoudiger annuleringsbewuste toepassingen en bibliotheken maken en ondersteunt het de volgende functies:

  • Annulering is coöperatief en wordt niet gedwongen aan de listener. De listener bepaalt hoe het probleemloos wordt beëindigd als reactie op een annuleringsaanvraag.

  • Aanvragen verschillen van luisteren. Een object dat een annuleringsbewerking aanroept, kan bepalen wanneer (indien ooit) annulering wordt aangevraagd.

  • Het aanvragende object geeft de annuleringsaanvraag uit aan alle kopieën van het token met behulp van slechts één methode-aanroep.

  • Een listener kan tegelijkertijd naar meerdere tokens luisteren door ze aan één gekoppeld token toe te voegen.

  • Gebruikerscode kan annuleringsaanvragen van bibliotheekcode opmerken en erop reageren, en bibliotheekcode kan annuleringsaanvragen van gebruikerscode opmerken en erop reageren.

  • Listeners kunnen op de hoogte worden gesteld van annuleringsaanvragen door polling, callback-registratie of wachten op wachtgrepen.

Annuleringstypen

Het annuleringsframework wordt geïmplementeerd als een set gerelateerde typen, die worden vermeld in de volgende tabel.

Typenaam Beschrijving
CancellationTokenSource Object dat een annuleringstoken maakt en ook de annuleringsaanvraag voor alle kopieën van dat token uitgeeft.
CancellationToken Lichtgewicht waardetype dat wordt doorgegeven aan een of meer listeners, meestal als methodeparameter. Listeners controleren de waarde van de IsCancellationRequested eigenschap van het token door polling, callback of wachtgreep.
OperationCanceledException Overbelastingen van de constructor van deze uitzondering accepteren een CancellationToken parameter. Listeners kunnen deze uitzondering eventueel genereren om de bron van de annulering te controleren en anderen te informeren dat deze heeft gereageerd op een annuleringsaanvraag.

Het annuleringsmodel is in verschillende typen geïntegreerd in .NET. De belangrijkste zijn System.Threading.Tasks.Parallel, en System.Threading.Tasks.Task<TResult>System.Threading.Tasks.TaskSystem.Linq.ParallelEnumerable. U wordt aangeraden dit coöperatieve annuleringsmodel te gebruiken voor alle nieuwe bibliotheek- en toepassingscode.

Codevoorbeeld

In het volgende voorbeeld maakt het aanvragende object een CancellationTokenSource object en geeft de eigenschap vervolgens door Token aan de annuleringsbewerking. De bewerking die de aanvraag ontvangt, bewaakt de waarde van de IsCancellationRequested eigenschap van het token door te pollen. Wanneer de waarde wordt true, kan de listener op welke manier dan ook worden beëindigd. In dit voorbeeld wordt de methode gewoon afgesloten. Dit is alles wat in veel gevallen vereist is.

Notitie

In het voorbeeld wordt de QueueUserWorkItem methode gebruikt om aan te tonen dat het samenwerkingsannuleringsframework compatibel is met verouderde API's. Zie Procedure voor een voorbeeld waarin het voorkeurstype System.Threading.Tasks.Taskwordt gebruikt: Een taak en de onderliggende items annuleren.

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...

Annulering van bewerking versus objectannulering

In het coöperatief annuleringskader verwijst annulering naar bewerkingen, niet naar objecten. De annuleringsaanvraag betekent dat de bewerking zo snel mogelijk moet stoppen nadat een vereiste opschoonbewerking is uitgevoerd. Eén annuleringstoken moet verwijzen naar één 'annuleerbare bewerking', maar die bewerking kan worden geïmplementeerd in uw programma. Nadat de IsCancellationRequested eigenschap van het token is ingesteld op , kan het niet opnieuw worden ingesteld trueop false. Daarom kunnen annuleringstokens niet opnieuw worden gebruikt nadat ze zijn geannuleerd.

Als u een mechanisme voor objectannulatie nodig hebt, kunt u dit baseren op het annuleringsmechanisme van de bewerking door de CancellationToken.Register methode aan te roepen, zoals wordt weergegeven in het volgende voorbeeld.

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

Als een object meer dan één gelijktijdige annuleerbare bewerking ondersteunt, geeft u een afzonderlijk token door als invoer voor elke afzonderlijke annuleringsbewerking. Op die manier kan één bewerking worden geannuleerd zonder dat dit van invloed is op de andere bewerkingen.

Luisteren en reageren op annuleringsaanvragen

In de gemachtigde van de gebruiker bepaalt de implementeerfunctie van een geannuleerde bewerking hoe de bewerking moet worden beëindigd als reactie op een annuleringsaanvraag. In veel gevallen kan de gemachtigde van de gebruiker gewoon elke vereiste opschoning uitvoeren en vervolgens onmiddellijk terugkeren.

In complexere gevallen kan het echter nodig zijn dat de gemachtigde van de gebruiker bibliotheekcode op de hoogte stelt van de annulering. In dergelijke gevallen is de juiste manier om de bewerking te beëindigen, de gedelegeerde de methode aan te roepen ThrowIfCancellationRequested, waardoor er een OperationCanceledException wordt gegenereerd. Bibliotheekcode kan deze uitzondering ondervangen op de thread van de gemachtigde gebruiker en het token van de uitzondering onderzoeken om te bepalen of de uitzondering aangeeft dat de uitzondering opzeggend is of een andere uitzonderlijke situatie.

De Task klasse verwerkt OperationCanceledException op deze manier. Zie Taakannulering voor meer informatie.

Luisteren door polling

Voor langdurige berekeningen die lus of recurse uitvoeren, kunt u luisteren naar een annuleringsaanvraag door periodiek de waarde van de CancellationToken.IsCancellationRequested eigenschap te peilen. Als de waarde is true, moet de methode zo snel mogelijk worden opgeschoond en beëindigd. De optimale frequentie van polling is afhankelijk van het type toepassing. Het is aan de ontwikkelaar om de beste pollingfrequentie voor een bepaald programma te bepalen. Polling zelf heeft geen aanzienlijke invloed op de prestaties. In het volgende voorbeeld ziet u een mogelijke manier om te peilen.

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

Zie Procedure voor een volledig voorbeeld: Luisteren naar annuleringsaanvragen door te peilen.

Luisteren door een callback te registreren

Sommige bewerkingen kunnen zodanig worden geblokkeerd dat ze de waarde van het annuleringstoken niet tijdig kunnen controleren. Voor deze gevallen kunt u een callback-methode registreren waarmee de methode wordt gedeblokkeerd wanneer een annuleringsaanvraag wordt ontvangen.

De Register methode retourneert een CancellationTokenRegistration object dat specifiek voor dit doel wordt gebruikt. In het volgende voorbeeld ziet u hoe u de Register methode gebruikt om een asynchrone webaanvraag te annuleren.

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

Het CancellationTokenRegistration object beheert threadsynchronisatie en zorgt ervoor dat de callback op een nauwkeurig tijdstip stopt met uitvoeren.

Om de reactiesnelheid van het systeem te waarborgen en impasses te voorkomen, moeten de volgende richtlijnen worden gevolgd bij het registreren van callbacks:

  • De callback-methode moet snel zijn omdat deze synchroon wordt aangeroepen en daarom wordt de aanroep naar Cancel niet geretourneerd totdat de callback wordt geretourneerd.

  • Als u belt Dispose terwijl de callback wordt uitgevoerd en u een vergrendeling vasthoudt waarop de callback wacht, kan uw programma een impasse hebben. Nadat Dispose het resultaat is geretourneerd, kunt u alle resources die nodig zijn voor de callback gratis maken.

  • Callbacks mogen geen handmatige threads of SynchronizationContext gebruik uitvoeren in een callback. Als een callback moet worden uitgevoerd op een bepaalde thread, gebruikt u de System.Threading.CancellationTokenRegistration constructor waarmee u kunt opgeven dat de doelsyncContext de actieve SynchronizationContext.Currentis. Handmatige threading uitvoeren in een callback kan een impasse veroorzaken.

Zie Procedure: Callbacks registreren voor annuleringsaanvragen voor een volledig voorbeeld.

Luisteren met behulp van een wachtgreep

Wanneer een geannuleerde bewerking kan worden geblokkeerd terwijl deze wacht op een synchronisatieprimitief, zoals a System.Threading.ManualResetEvent of System.Threading.Semaphore, kunt u de CancellationToken.WaitHandle eigenschap gebruiken om de bewerking in te schakelen om te wachten op zowel de gebeurtenis als de annuleringsaanvraag. De wachtgreep van het annuleringstoken wordt gesignaleerd als reactie op een annuleringsaanvraag en de methode kan de retourwaarde van de WaitAny methode gebruiken om te bepalen of het het annuleringstoken is dat is gesignaleerd. De bewerking kan vervolgens gewoon afsluiten of een OperationCanceledException, indien van toepassing.

// 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 en System.Threading.SemaphoreSlim beide ondersteunen het annuleringsframework in hun Wait methoden. U kunt de CancellationToken methode doorgeven en wanneer de annulering wordt aangevraagd, wordt de gebeurtenis wakker en wordt er een 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)

Zie Procedure voor een volledig voorbeeld: Luisteren naar annuleringsaanvragen met wachtgrepen.

Gelijktijdig luisteren naar meerdere tokens

In sommige gevallen moet een listener mogelijk tegelijkertijd naar meerdere annuleringstokens luisteren. Een annuleerbare bewerking moet bijvoorbeeld een intern annuleringstoken bewaken naast een token dat extern is doorgegeven als argument voor een methodeparameter. Hiervoor maakt u een gekoppelde tokenbron die twee of meer tokens aan één token kan koppelen, zoals wordt weergegeven in het volgende voorbeeld.

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

U moet de gekoppelde tokenbron aanroepen Dispose wanneer u er klaar mee bent. Zie Procedure voor een volledig voorbeeld: Luisteren naar meerdere annuleringsaanvragen.

Samenwerking tussen bibliotheekcode en gebruikerscode

Het geïntegreerde annuleringsframework maakt het mogelijk om bibliotheekcode te annuleren van gebruikerscode en voor gebruikerscode om bibliotheekcode op een coöperatieve manier te annuleren. Een soepele samenwerking hangt af van elk van de volgende richtlijnen:

  • Als bibliotheekcode annuleringsbewerkingen biedt, moet deze ook openbare methoden bieden die een extern annuleringstoken accepteren, zodat gebruikerscode annulering kan aanvragen.

  • Als bibliotheekcode gebruikerscode aanroept, moet de bibliotheekcode een OperationCanceledException(externalToken) interpreteren als coöperatieve annulering en niet noodzakelijkerwijs als een foutuitzondering.

  • Gebruikersdelegen moeten tijdig proberen te reageren op annuleringsaanvragen van bibliotheekcode.

System.Threading.Tasks.Task en System.Linq.ParallelEnumerable zijn voorbeelden van klassen die aan deze richtlijnen voldoen. Zie Taakannulering en Procedures voor meer informatie: Een PLINQ-query annuleren.

Zie ook