Udostępnij za pośrednictwem


Anulowanie w zarządzanych wątkach

Rozpoczynając od wersji 4 .NET Framework, platforma .NET używa ujednoliconego modelu współpracy przy anulowaniu operacji asynchronicznych lub długotrwałych operacji synchronicznych. Ten model jest oparty na uproszczonym obiekcie nazywanym tokenem anulowania. Obiekt, który wywołuje co najmniej jedną operację, którą można anulować, na przykład tworząc nowe wątki lub zadania, przekazuje token do każdej operacji. Poszczególne operacje mogą z kolei przekazywać kopie tokenu do innych operacji. W późniejszym czasie obiekt, który utworzył token, może użyć go do żądania, aby operacje zatrzymały to, co robią. Tylko obiekt żądający może wydać żądanie anulowania, a każdy odbiornik jest odpowiedzialny za zauważenie żądania i odpowiadanie na nie w odpowiedni i terminowy sposób.

Ogólny wzorzec wdrażania kooperacyjnego modelu anulowania wygląda następująco:

  • Inicjuje obiekt CancellationTokenSource, który zarządza i przesyła powiadomienie o anulowaniu do poszczególnych tokenów anulujących.

  • Przekaż token zwrócony przez CancellationTokenSource.Token właściwość do każdego zadania lub wątku, który oczekuje anulowania.

  • Podaj mechanizm dla każdego zadania lub wątku, aby reagować na anulowanie.

  • Wywołaj metodę CancellationTokenSource.Cancel, aby przekazać powiadomienie o anulowaniu.

Ważne

Klasa CancellationTokenSource implementuje interfejs IDisposable. Zadbaj o wywołanie metody CancellationTokenSource.Dispose po zakończeniu używania źródła tokenu anulowania, aby zwolnić wszystkie niezarządzane zasoby, które przechowuje.

Poniższa ilustracja przedstawia relację między źródłem tokenu a wszystkimi kopiami tokenu.

CancellationTokenSource i tokeny anulowania

Model współbieżnego anulowania ułatwia tworzenie aplikacji i bibliotek z obsługą anulowania oraz obsługuje następujące funkcje:

  • Anulowanie jest współpracujące i nie jest wymuszone na słuchaczu. Odbiornik określa, jak bezpiecznie zakończyć odpowiedź na żądanie anulowania.

  • Żądanie różni się od nasłuchiwania. Obiekt, który wywołuje operację, którą można anulować, może kontrolować, kiedy (jeśli kiedykolwiek) zażądano anulowania.

  • Obiekt żądający wysyła żądanie anulowania do wszystkich kopii tokenu przy użyciu tylko jednego wywołania metody.

  • Odbiornik może nasłuchiwać wielu tokenów jednocześnie, dołączając je do jednego połączonego tokenu.

  • Kod użytkownika może zauważyć żądania anulowania z kodu biblioteki i odpowiadać na nie, a kod biblioteki może zauważyć i odpowiadać na żądania anulowania z kodu użytkownika.

  • Odbiorniki mogą być powiadamiane o żądaniach anulowania przez sondowanie, rejestrację wywołania zwrotnego lub oczekiwanie na dojścia oczekiwania.

Typy anulowania

Struktura anulowania jest implementowana jako zestaw powiązanych typów wymienionych w poniższej tabeli.

Nazwa typu Opis
CancellationTokenSource Obiekt, który tworzy token anulowania, a także wystawia żądanie anulowania dla wszystkich kopii tego tokenu.
CancellationToken Lekki typ wartości przekazywany do jednego lub więcej słuchaczy, zazwyczaj jako parametr metody. Odbiorniki monitorują wartość właściwości IsCancellationRequested tokenu za pomocą odpytywania, wywołań zwrotnych lub mechanizmu oczekiwania.
OperationCanceledException Przeciążenia konstruktora tego wyjątku akceptują CancellationToken jako parametr. Słuchacze mogą opcjonalnie zgłaszać ten wyjątek, aby zweryfikować źródło anulowania i powiadomić inne osoby, że zareagowano na żądanie anulowania.

Model anulowania jest zintegrowany z platformą .NET w kilku typach. Najważniejsze z nich to System.Threading.Tasks.Parallel, System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> i System.Linq.ParallelEnumerable. Zalecamy użycie tego modelu anulowania współpracy dla wszystkich nowych bibliotek i kodu aplikacji.

Przykład kodu

W poniższym przykładzie obiekt żądający tworzy obiekt CancellationTokenSource, a następnie przekazuje jego właściwość Token do operacji, która może zostać anulowana. Operacja, która odbiera żądanie, monitoruje wartość właściwości IsCancellationRequested tokenu przez sondowanie. Gdy wartość osiągnie true, nasłuchujący może zakończyć działanie w dowolny sposób. W tym przykładzie metoda po prostu kończy działanie, co jest wymagane w wielu przypadkach.

Uwaga / Notatka

W przykładzie użyto metody QueueUserWorkItem, aby zademonstrować, że model współpracy przy anulowaniu jest zgodny ze starszymi interfejsami API. Aby zapoznać się z przykładem, który używa preferowanego System.Threading.Tasks.Task typu, zobacz Jak: Anulować zadanie i jego elementy podrzędne.

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

Anulowanie operacji kontra anulowanie obiektu

W ramach struktury anulowania w kooperatywie, anulowanie odnosi się do operacji, a nie obiektów. Żądanie anulowania oznacza, że operacja powinna zostać zatrzymana tak szybko, jak to możliwe po wykonaniu wymaganego czyszczenia. Jeden token anulowania powinien odwoływać się do jednej "operacji anulowania", jednak ta operacja może zostać zaimplementowana w programie. Po ustawieniu IsCancellationRequested właściwości tokenu na true, nie można go zresetować do false. W związku z tym tokeny anulowania nie mogą być ponownie używane po anulowaniu.

Jeśli potrzebujesz mechanizmu anulowania obiektu, możesz go oprzeć na mechanizmie anulowania operacji, wywołując metodę CancellationToken.Register , jak pokazano w poniższym przykładzie.

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

Jeśli obiekt obsługuje więcej niż jedną operację z możliwością anulowania współbieżnego, przekaż oddzielny token jako dane wejściowe do każdej odrębnej operacji anulowania. W ten sposób można anulować jedną operację bez wpływu na inne.

Nasłuchiwanie żądań anulowania i odpowiadanie na nie

W delegacji użytkownika implementator operacji anulowania określa, jak zakończyć operację w odpowiedzi na żądanie anulowania. W wielu przypadkach delegat użytkownika może po prostu przeprowadzić wymagane czyszczenie, a następnie natychmiast przejść do kolejnego zadania.

Jednak w bardziej złożonych przypadkach może być konieczne, aby delegat użytkownika powiadamiał kod biblioteki, że nastąpiło anulowanie. W takich przypadkach prawidłowym sposobem zakończenia operacji jest, aby delegat wywołał metodę ThrowIfCancellationRequested, co spowoduje zgłoszenie OperationCanceledException. Kod biblioteki może przechwycić ten wyjątek w wątku delegata użytkownika i zbadać token wyjątku, aby określić, czy wyjątek wskazuje na anulowanie współpracy, czy też inną wyjątkową sytuację.

Klasa Task obsługuje OperationCanceledException w ten sposób. Aby uzyskać więcej informacji, zobacz Anulowanie zadania.

Nasłuchiwanie przez sondowanie

W przypadku długotrwałych obliczeń w pętlach lub rekursji, można nasłuchiwać żądania anulowania, okresowo sprawdzając wartość właściwości CancellationToken.IsCancellationRequested. Jeśli jej wartość to true, metoda powinna oczyścić i zakończyć tak szybko, jak to możliwe. Optymalna częstotliwość sondowania zależy od typu aplikacji. Do dewelopera należy określenie najlepszej częstotliwości sondowania dla danego programu. Samo sondowanie nie ma znaczącego wpływu na wydajność. Poniższy przykład przedstawia jeden z możliwych sposobów sondowania.

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

Aby uzyskać bardziej kompletny przykład, zobacz Instrukcje: wykrywanie żądań anulowania przez sondowanie.

Nasłuchiwanie poprzez zarejestrowanie wywołania zwrotnego

Niektóre operacje mogą zostać zablokowane w taki sposób, że nie mogą sprawdzić wartości tokenu anulowania w odpowiednim czasie. W takich przypadkach można zarejestrować metodę wywołania zwrotnego, która odblokuje metodę po odebraniu żądania anulowania.

Metoda Register zwraca CancellationTokenRegistration obiekt, który jest używany specjalnie do tego celu. W poniższym przykładzie pokazano, jak za pomocą Register metody anulować asynchroniczne żądanie internetowe.

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

Obiekt CancellationTokenRegistration zarządza synchronizacją wątków i zapewnia, że wywołanie zwrotne przestanie działać w określonym punkcie w czasie.

Aby zapewnić czas reakcji systemu i uniknąć zakleszczenia, podczas rejestrowania wywołań zwrotnych należy przestrzegać następujących wytycznych:

  • Metoda wywołania zwrotnego powinna być szybka, ponieważ jest wywoływana synchronicznie i dlatego wywołanie Cancel nie kończy się, dopóki wywołanie zwrotne się nie zakończy.

  • Jeśli wywołasz Dispose w czasie wykonywania wywołania zwrotnego i trzymasz blokadę, na którą oczekuje wywołanie zwrotne, twój program może się zakleszczyć. Po Dispose powrocie możesz zwolnić wszystkie zasoby wymagane przez wywołanie zwrotne.

  • Funkcje zwrotne nie powinny wykonywać żadnych operacji ręcznych związanych z wątkami ani korzystać z SynchronizationContext w funkcji zwrotnej. Jeśli wywołanie zwrotne musi być uruchomione w określonym wątku, użyj konstruktora System.Threading.CancellationTokenRegistration, który umożliwia określenie, że docelowy kontekst synchronizacji jest aktywny SynchronizationContext.Current. Wykonywanie ręcznego wątkowania w wywołaniu zwrotnym może spowodować zawieszenie.

Aby uzyskać bardziej kompletny przykład, zobacz Instrukcje: rejestrowanie wywołań zwrotnych dla żądań anulowania.

Nasłuchiwanie przy użyciu uchwytu do oczekiwania

Gdy operacja z możliwością anulowania może się zablokować, czekając na prymityw synchronizacji, taki jak System.Threading.ManualResetEvent lub System.Threading.Semaphore, można skorzystać z właściwości CancellationToken.WaitHandle, aby umożliwić operacji oczekiwanie zarówno na zdarzenie, jak i żądanie anulowania. Obsługa oczekiwania na token anulowania zostanie zasygnalizowana w odpowiedzi na żądanie anulowania, a metoda może użyć wartości zwracanej przez metodę WaitAny, aby określić, czy to token anulowania zasygnalizował. Odpowiednio, operacja może następnie zakończyć działanie lub zgłosić OperationCanceledException.

// 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 i System.Threading.SemaphoreSlim oba obsługują strukturę anulowania w swoich Wait metodach. Możesz przekazać metodę CancellationToken do metody , a po żądaniu anulowania zdarzenie wznawia się i zgłasza błąd 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)

Aby uzyskać bardziej kompletny przykład, zobacz How to: Listen for Cancellation Requests That Have Wait Handles (Instrukcje: nasłuchiwanie żądań anulowania, które mają uchwyty oczekiwania).

Nasłuchiwanie wielu tokenów jednocześnie

W niektórych przypadkach odbiornik może wymagać jednoczesnego nasłuchiwania wielu tokenów anulowania. Na przykład operacja umożliwiająca anulowanie może wymagać monitorowania wewnętrznego tokenu anulowania oprócz tokenu przekazanego zewnętrznie jako argumentu do parametru metody. W tym celu utwórz połączone źródło tokenu, które może łączyć dwa lub więcej tokenów w jeden token, jak pokazano w poniższym przykładzie.

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

Należy zwrócić uwagę, że po zakończeniu pracy z połączonym źródłem tokenu, należy wywołać Dispose. Aby uzyskać bardziej kompletny przykład, zobacz Instrukcje: nasłuchiwanie wielu żądań anulowania.

Współpraca między kodem biblioteki a kodem użytkownika

Ujednolicona struktura anulowania umożliwia kodowi biblioteki anulowanie kodu użytkownika, a kodowi użytkownika anulowanie kodu biblioteki w sposób współpracy. Płynna współpraca zależy od przestrzegania przez każdą stronę następujących wytycznych:

  • Jeśli kod biblioteki zapewnia operacje anulowania, powinien również udostępnić publiczne metody, które akceptują zewnętrzny token anulowania, aby kod użytkownika mógł zażądać anulowania.

  • Jeśli kod biblioteki wywołuje kod użytkownika, kod biblioteki powinien interpretować wyjątek OperationCanceledException(externalToken) jako anulowanie współpracy, a niekoniecznie jako wyjątek niepowodzenia.

  • Delegaci użytkownika powinni próbować reagować na żądania anulowania z kodu biblioteki w odpowiednim czasie.

System.Threading.Tasks.Task i System.Linq.ParallelEnumerable są przykładami klas, które są zgodne z tymi wytycznymi. Aby uzyskać więcej informacji, zobacz Anulowanie zadań i Instrukcje: Anulowanie zapytania PLINQ.

Zobacz także