Anulowanie w zarządzanych wątkach

Począwszy od programu .NET Framework 4, platforma .NET używa ujednoliconego modelu do anulowania 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 modelu anulowania współpracy to:

  • Utworzenie wystąpienia CancellationTokenSource obiektu, który zarządza i wysyła powiadomienie o anulowaniu do poszczególnych tokenów anulowania.

  • Przekaż token zwrócony przez CancellationTokenSource.Token właściwość do każdego zadania lub wątku, który nasłuchuje 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. Pamiętaj, aby wywołać metodę po zakończeniu CancellationTokenSource.Dispose korzystania ze ź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 and cancellation tokens

Model anulowania współpracy ułatwia tworzenie aplikacji i bibliotek obsługujących anulowanie oraz obsługuje następujące funkcje:

  • Anulowanie jest współpracy i nie jest wymuszone na odbiorniku. 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 Uproszczony typ wartości przekazywany do co najmniej jednego odbiornika, zazwyczaj jako parametr metody. Odbiorniki monitorują wartość IsCancellationRequested właściwości tokenu przez sondowanie, wywołanie zwrotne lub uchwyt oczekiwania.
OperationCanceledException Przeciążenia konstruktora tego wyjątku akceptują CancellationToken jako parametr. Odbiorniki mogą opcjonalnie zgłaszać ten wyjątek, aby zweryfikować źródło anulowania i powiadomić inne osoby, że odpowiedziały 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 CancellationTokenSource obiekt, a następnie przekazuje jego Token właściwość do operacji anulowania. Operacja odbierana przez żądanie monitoruje wartość IsCancellationRequested właściwości tokenu przez sondowanie. Gdy wartość stanie się truewartością , odbiornik 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

W przykładzie użyto QueueUserWorkItem metody , aby zademonstrować, że platforma anulowania współpracy jest zgodna ze starszymi interfejsami API. Aby zapoznać się z przykładem, który używa preferowanego System.Threading.Tasks.Task typu, zobacz Instrukcje: Anulowanie zadania i jego elementów podrzędnych.

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

Anulowanie operacji a anulowanie obiektu

W ramach anulowania współpracy 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 {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

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 wykonać dowolne wymagane czyszczenie, a następnie natychmiast zwrócić.

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 wywołanie ThrowIfCancellationRequestedmetody , która spowoduje OperationCanceledException zgłoszenie operacji przez delegata. 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ń pętli lub cykli można nasłuchiwać żądania anulowania, okresowo sondując wartość CancellationToken.IsCancellationRequested właściwości. 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 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

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

Nasłuchiwanie przez 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 sieci Web.

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

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, należy przestrzegać następujących wytycznych podczas rejestrowania wywołań zwrotnych:

  • Metoda wywołania zwrotnego powinna być szybka, ponieważ jest wywoływana synchronicznie i dlatego wywołanie Cancel metody nie zwraca się, dopóki wywołanie zwrotne nie zostanie zwrócone.

  • Jeśli wywołasz Dispose wywołanie zwrotne, gdy wywołanie zwrotne jest uruchomione, a blokada, na którą czeka wywołanie zwrotne, program może zakleszczeć. Po Dispose powrocie możesz zwolnić wszystkie zasoby wymagane przez wywołanie zwrotne.

  • Wywołania zwrotne nie powinny wykonywać żadnych ręcznych wątków ani SynchronizationContext użycia w wywołaniu zwrotnym. Jeśli wywołanie zwrotne musi być uruchamiane w określonym wątku, użyj System.Threading.CancellationTokenRegistration konstruktora, który umożliwia określenie, że docelowa synchronizacjaContext jest aktywna SynchronizationContext.Current. Wykonywanie ręcznego wątkowania w wywołaniu zwrotnym może spowodować zakleszczenie.

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

Nasłuchiwanie przy użyciu uchwytu oczekiwania

Gdy operacja z możliwością anulowania może blokować czas oczekiwania na element pierwotny synchronizacji, taki jak lub System.Threading.Semaphore, można użyć CancellationToken.WaitHandle właściwości , aby umożliwić operację oczekiwania zarówno na zdarzenie, jak System.Threading.ManualResetEvent i żądanie anulowania. Obsługa oczekiwania tokenu anulowania zostanie zasygnalizowana w odpowiedzi na żądanie anulowania, a metoda może użyć wartości WaitAny zwracanej metody, aby określić, czy był to token anulowania, który zasygnalizował. Operacja może następnie zakończyć działanie lub zgłosić OperationCanceledExceptionelement , odpowiednio.

// 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 obie System.Threading.SemaphoreSlim 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

Zwróć uwagę, że po zakończeniu pracy należy wywołać Dispose źródło połączonego tokenu. 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 anulowanie kodu biblioteki oraz anulowanie kodu użytkownika w celu anulowania kodu biblioteki w sposób współpracy. Płynna współpraca zależy od każdej strony, zgodnie z następującymi wytycznymi:

  • 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 też