Dela via


Annullering i hanterade trådar

Från och med .NET Framework 4 använder .NET en enhetlig modell för kooperativ annullering av asynkrona eller långvariga synkrona åtgärder. Den här modellen baseras på ett lättviktsobjekt som kallas för en annulleringstoken. Objektet som anropar en eller flera avbrutna åtgärder, till exempel genom att skapa nya trådar eller uppgifter, skickar token till varje åtgärd. Enskilda åtgärder kan i sin tur skicka kopior av token till andra åtgärder. Vid ett senare tillfälle kan objektet som skapade token använda den för att begära att åtgärderna stoppar det de gör. Endast det begärande objektet kan utfärda annulleringsbegäran och varje lyssnare ansvarar för att märka begäran och svara på den på ett lämpligt och lämpligt sätt i rätt tid.

Det allmänna mönstret för att implementera modellen för kooperativ annullering är:

  • Instansiera ett CancellationTokenSource objekt som hanterar och skickar aviseringar om annullering till de enskilda annulleringstoken.

  • Skicka token som returneras av CancellationTokenSource.Token egenskapen till varje uppgift eller tråd som lyssnar efter annullering.

  • Ange en mekanism för varje uppgift eller tråd för att svara på annullering.

  • CancellationTokenSource.Cancel Anropa metoden för att meddela om annullering.

Viktigt!

Klassen CancellationTokenSource implementerar gränssnittet IDisposable. Du bör vara noga med att anropa CancellationTokenSource.Dispose metoden när du är klar med att använda källan för annulleringstoken för att frigöra ohanterade resurser som den innehåller.

Följande bild visar relationen mellan en tokenkälla och alla kopior av dess token.

CancellationTokenSource and cancellation tokens

Modellen för kooperativ annullering gör det enklare att skapa program och bibliotek som är medvetna om annullering och stöder följande funktioner:

  • Annulleringen är samarbetsinriktad och tvingas inte på lyssnaren. Lyssnaren bestämmer hur du korrekt avslutar som svar på en annulleringsbegäran.

  • Att begära skiljer sig från att lyssna. Ett objekt som anropar en avbruten åtgärd kan styra när (om någonsin) annullering begärs.

  • Det begärande objektet utfärdar annulleringsbegäran till alla kopior av token med bara ett metodanrop.

  • En lyssnare kan lyssna på flera token samtidigt genom att ansluta dem till en länkad token.

  • Användarkod kan märka och svara på begäranden om annullering från bibliotekskod, och bibliotekskoden kan märka och svara på begäranden om annullering från användarkoden.

  • Lyssnare kan meddelas om avbokningsbegäranden genom avsökning, återanropsregistrering eller väntar på väntehandtag.

Annulleringstyper

Ramverket för annullering implementeras som en uppsättning relaterade typer som visas i följande tabell.

Typnamn beskrivning
CancellationTokenSource Objekt som skapar en annulleringstoken och även utfärdar begäran om annullering för alla kopior av denna token.
CancellationToken Lätt värdetyp som skickas till en eller flera lyssnare, vanligtvis som en metodparameter. Lyssnare övervakar värdet för IsCancellationRequested egenskapen för token genom avsökning, återanrop eller väntehandtag.
OperationCanceledException Överlagringar av det här undantagets konstruktor accepterar en CancellationToken som en parameter. Lyssnare kan också utlösa det här undantaget för att verifiera källan till annulleringen och meddela andra att de har svarat på en begäran om annullering.

Annulleringsmodellen är integrerad i .NET i flera typer. De viktigaste är System.Threading.Tasks.Parallel, System.Threading.Tasks.Taskoch System.Threading.Tasks.Task<TResult>System.Linq.ParallelEnumerable. Vi rekommenderar att du använder den här modellen för kooperativ annullering för alla nya bibliotek och programkod.

Kodexempel

I följande exempel skapar det begärande objektet ett CancellationTokenSource objekt och skickar sedan dess Token egenskap till den avbrutna åtgärden. Åtgärden som tar emot begäran övervakar värdet IsCancellationRequested för egenskapen för token genom avsökning. När värdet blir truekan lyssnaren avsluta på det sätt som är lämpligt. I det här exemplet avslutas metoden bara, vilket är allt som krävs i många fall.

Kommentar

I exemplet används QueueUserWorkItem metoden för att visa att ramverket för kooperativ annullering är kompatibelt med äldre API:er. Ett exempel som använder önskad System.Threading.Tasks.Task typ finns i Så här: Avbryt en aktivitet och Dess underordnade.

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

Annullering av åtgärd jämfört med annullering av objekt

I ramverket för kooperativ annullering avser annullering åtgärder, inte objekt. Annulleringsbegäran innebär att åtgärden ska stoppas så snart som möjligt efter att nödvändig rensning har utförts. En annulleringstoken bör referera till en "avbrutbar åtgärd", men den åtgärden kan implementeras i programmet. IsCancellationRequested När egenskapen för token har angetts till truekan den inte återställas till false. Därför kan inte annulleringstoken återanvändas efter att de har avbrutits.

Om du behöver en mekanism för att avbryta objekt kan du basera den på åtgärdens annulleringsmekanism genom att anropa CancellationToken.Register metoden, som du ser i följande exempel.

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

Om ett objekt stöder mer än en samtidig avbrutbar åtgärd skickar du en separat token som indata till varje distinkt avbruten åtgärd. På så sätt kan en åtgärd avbrytas utan att påverka de andra.

Lyssna och svara på begäranden om annullering

I användardelegaten bestämmer implementeraren för en avbruten åtgärd hur åtgärden ska avslutas som svar på en begäran om annullering. I många fall kan användardelegaten bara utföra alla nödvändiga rensningar och sedan returnera omedelbart.

I mer komplexa fall kan det dock vara nödvändigt för användardelegaten att meddela bibliotekskoden om att det har skett en annullering. I sådana fall är det rätta sättet att avsluta åtgärden att ombudet anropar ThrowIfCancellationRequestedmetoden , vilket gör att en OperationCanceledException genereras. Bibliotekskoden kan fånga det här undantaget i användardelegattråden och undersöka undantagets token för att avgöra om undantaget anger att kooperativet har avbrutits eller någon annan exceptionell situation.

Klassen Task hanterar OperationCanceledException på det här sättet. Mer information finns i Annullering av aktiviteter.

Lyssnar efter avsökning

För långvariga beräkningar som loopar eller upprepas kan du lyssna efter en begäran om annullering genom att regelbundet avsöka värdet för CancellationToken.IsCancellationRequested egenskapen. Om dess värde är truebör metoden rensas och avslutas så snabbt som möjligt. Den optimala avsökningsfrekvensen beror på typen av program. Det är upp till utvecklaren att fastställa den bästa avsökningsfrekvensen för ett visst program. Själva avsökningen påverkar inte prestandan nämnvärt. I följande exempel visas ett möjligt sätt att avsöka.

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

Ett mer komplett exempel finns i Så här: Lyssna efter annulleringsbegäranden efter avsökning.

Lyssna genom att registrera ett återanrop

Vissa åtgärder kan blockeras på ett sådant sätt att de inte kan kontrollera värdet för annulleringstoken i tid. I dessa fall kan du registrera en återanropsmetod som avblockera metoden när en begäran om annullering tas emot.

Metoden Register returnerar ett CancellationTokenRegistration objekt som används specifikt för detta ändamål. I följande exempel visas hur du använder Register metoden för att avbryta en asynkron webbbegäran.

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

Objektet CancellationTokenRegistration hanterar trådsynkronisering och ser till att återanropet slutar köras vid en exakt tidpunkt.

För att säkerställa systemets svarstider och undvika dödlägen måste följande riktlinjer följas vid registrering av återanrop:

  • Återanropsmetoden bör vara snabb eftersom den anropas synkront och därför returneras inte anropet till Cancel förrän återanropet returneras.

  • Om du anropar Dispose medan återanropet körs och du har ett lås som återanropet väntar på kan programmet blockeras. När Dispose returen har returnerats kan du frigöra alla resurser som krävs av återanropet.

  • Återanrop bör inte utföra någon manuell tråd eller SynchronizationContext användning i ett återanrop. Om ett återanrop måste köras på en viss tråd använder System.Threading.CancellationTokenRegistration du konstruktorn som gör att du kan ange att målsynkroniseringenContext är aktiv SynchronizationContext.Current. Om du utför manuell trådning i ett återanrop kan det orsaka ett dödläge.

Ett mer komplett exempel finns i How to: Register Callbacks for Cancellation Requests (Så här registrerar du återanrop för annulleringsbegäranden).

Lyssna med hjälp av ett väntehandtag

När en avbruten åtgärd kan blockeras medan den väntar på en synkroniseringsprimär, till exempel en System.Threading.ManualResetEvent eller System.Threading.Semaphore, kan du använda CancellationToken.WaitHandle egenskapen för att aktivera åtgärden för att vänta på både händelsen och annulleringsbegäran. Väntehandtaget för annulleringstoken kommer att signaleras som svar på en annulleringsbegäran och metoden kan använda returvärdet WaitAny för metoden för att avgöra om det var annulleringstoken som signalerade. Åtgärden kan sedan bara avsluta eller utlösa en OperationCanceledException, efter behov.

// 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 och System.Threading.SemaphoreSlim båda stöder annulleringsramverket i sina Wait metoder. Du kan skicka CancellationToken till -metoden, och när annulleringen begärs vaknar händelsen och genererar en 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)

Ett mer komplett exempel finns i Så här: Lyssna efter annulleringsbegäranden som har väntehandtag.

Lyssna på flera token samtidigt

I vissa fall kan en lyssnare behöva lyssna på flera annulleringstoken samtidigt. En avbruten åtgärd kan till exempel behöva övervaka en intern annulleringstoken utöver en token som skickas externt som ett argument till en metodparameter. För att åstadkomma detta skapar du en länkad tokenkälla som kan koppla två eller flera token till en token, enligt följande exempel.

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

Observera att du måste anropa Dispose den länkade tokenkällan när du är klar med den. Ett mer komplett exempel finns i Så här: Lyssna efter flera annulleringsbegäranden.

Samarbete mellan bibliotekskod och användarkod

Det enhetliga ramverket för annullering gör det möjligt för bibliotekskoden att avbryta användarkoden och att användarkoden avbryter bibliotekskoden på ett samarbetsmässigt sätt. Ett smidigt samarbete är beroende av att båda sidor följer dessa riktlinjer:

  • Om bibliotekskoden innehåller avbrutna åtgärder bör den även tillhandahålla offentliga metoder som accepterar en extern annulleringstoken så att användarkoden kan begära annullering.

  • Om bibliotekskoden anropar användarkoden bör bibliotekskoden tolka en OperationCanceledException(externalToken) som kooperativ annullering och inte nödvändigtvis som ett felundantag.

  • Användardelegater bör försöka svara på begäranden om annullering från bibliotekskoden i tid.

System.Threading.Tasks.Task och System.Linq.ParallelEnumerable är exempel på klasser som följer dessa riktlinjer. Mer information finns i Annullering av aktiviteter och Så här: Avbryt en PLINQ-fråga.

Se även