Zrušení ve spravovaných vláknech

Počínaje rozhraním .NET Framework 4 používá .NET jednotný model pro kooperativní zrušení asynchronních nebo dlouhotrvajících synchronních operací. Tento model je založený na odlehčeném objektu označovaného jako token zrušení. Objekt, který vyvolá jednu nebo více stornovatelných operací, například vytvořením nových vláken nebo úloh, předá token každé operaci. Jednotlivé operace můžou předávat kopie tokenu jiným operacím. Objekt, který token vytvořil, ho může později použít k vyžádání toho, aby operace zastavily, co dělají. Žádost o zrušení může vydat pouze žádající objekt a každý naslouchací proces zodpovídá za zápis požadavku a odpovídá na něj odpovídajícím a včasným způsobem.

Obecný model implementace modelu zrušení spolupráce je:

  • Vytvořte instanci objektu CancellationTokenSource , který spravuje a odesílá oznámení o zrušení jednotlivým tokenům zrušení.

  • Předejte token vrácený vlastností každému CancellationTokenSource.Token úkolu nebo vláknu, které naslouchá zrušení.

  • Poskytněte mechanismus pro každou úlohu nebo vlákno pro reakci na zrušení.

  • Zavolejte metodu CancellationTokenSource.Cancel , která poskytuje oznámení o zrušení.

Důležité

Třída CancellationTokenSource implementuje rozhraní IDisposable. Nezapomeňte metodu CancellationTokenSource.Dispose volat, až dokončíte použití zdroje tokenu zrušení k uvolnění všech nespravovaných prostředků, které obsahuje.

Následující obrázek znázorňuje vztah mezi zdrojem tokenu a všemi kopiemi jeho tokenu.

CancellationTokenSource and cancellation tokens

Model spolupracujícího zrušení usnadňuje vytváření aplikací a knihoven podporujících zrušení a podporuje následující funkce:

  • Zrušení je družstevní a není nuceno naslouchacímu procesu. Naslouchací proces určuje, jak řádně ukončit v reakci na žádost o zrušení.

  • Žádosti se liší od naslouchání. Objekt, který vyvolá operaci zrušení, může řídit, kdy (pokud někdy) zrušení je požadováno.

  • Požadavek na objekt vydá žádost o zrušení pro všechny kopie tokenu pomocí jediného volání metody.

  • Naslouchací proces může současně naslouchat více tokenům tím, že je spojí do jednoho propojeného tokenu.

  • Uživatelský kód si může všimnout a reagovat na žádosti o zrušení z kódu knihovny a kód knihovny si může všimnout a reagovat na žádosti o zrušení z uživatelského kódu.

  • Naslouchací procesy můžou být upozorněny na žádosti o zrušení dotazováním, registrací zpětného volání nebo čekáním na obslužné rutiny čekání.

Typy zrušení

Architektura zrušení je implementována jako sada souvisejících typů, které jsou uvedeny v následující tabulce.

Název typu Popis
CancellationTokenSource Objekt, který vytvoří token zrušení, a také vydá žádost o zrušení pro všechny kopie tohoto tokenu.
CancellationToken Jednoduchý typ hodnoty předaný jednomu nebo více naslouchacím procesům, obvykle jako parametr metody. Naslouchací procesy monitorují hodnotu IsCancellationRequested vlastnosti tokenu dotazováním, zpětným voláním nebo popisovačem čekání.
OperationCanceledException Přetížení konstruktoru této výjimky přijímají CancellationToken jako parametr. Naslouchací procesy můžou volitelně vyvolat tuto výjimku, aby ověřili zdroj zrušení a upozornili ostatní, že odpověděli na žádost o zrušení.

Model zrušení je integrovaný do .NET v několika typech. Nejdůležitější jsou System.Threading.Tasks.Parallel, System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> a System.Linq.ParallelEnumerable. Doporučujeme použít tento model zrušení spolupráce pro všechny nové knihovny a kód aplikace.

Příklad kódu

V následujícím příkladu vytvoří požadovaný objekt CancellationTokenSource objekt a pak předá jeho Token vlastnost do zrušitelné operace. Operace, která obdrží požadavek, monitoruje hodnotu IsCancellationRequested vlastnosti tokenu dotazováním. Když se hodnota stane true, naslouchací proces může ukončit jakýmkoli způsobem, který je vhodný. V tomto příkladu se metoda právě ukončí, což je vše, co je v mnoha případech nutné.

Poznámka:

Příklad používá metodu QueueUserWorkItem k předvedení, že architektura pro kooperativní zrušení je kompatibilní se staršími rozhraními API. Příklad, který používá upřednostňovaný System.Threading.Tasks.Task typ, naleznete v tématu Postupy: Zrušení úkolu a jeho podřízených položek.

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

Zrušení operace versus zrušení objektu

V rámci zrušení spolupráce odkazuje zrušení na operace, nikoli na objekty. Žádost o zrušení znamená, že operace by se měla co nejdříve zastavit po provedení jakéhokoli požadovaného vyčištění. Jeden token zrušení by měl odkazovat na jednu operaci,", ale tato operace může být implementována ve vašem programu. IsCancellationRequested Jakmile je vlastnost tokenu nastavena truena , nelze ji obnovit na false. Proto po zrušení nelze znovu použít tokeny zrušení.

Pokud požadujete mechanismus zrušení objektu, můžete ho založit na mechanismu zrušení operace voláním CancellationToken.Register metody, jak je znázorněno v následujícím příkladu.

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

Pokud objekt podporuje více než jednu souběžnou operaci zrušení, předejte samostatný token jako vstup pro každou samostatnou zrušitelnou operaci. Tímto způsobem lze jednu operaci zrušit, aniž by to mělo vliv na ostatní.

Naslouchání a reagování na žádosti o zrušení

V delegátu uživatele implementátor zrušitelné operace určuje, jak ukončit operaci v reakci na požadavek zrušení. V mnoha případech může delegát uživatele pouze provést požadované vyčištění a okamžitě se vrátit.

V složitějších případech však může být nutné, aby delegát uživatele informoval kód knihovny, že došlo ke zrušení. V takových případech je správný způsob, jak ukončit operaci, je pro delegáta volání ThrowIfCancellationRequested, metoda, která způsobí OperationCanceledException vyvolání. Kód knihovny může zachytit tuto výjimku ve vlákně delegáta uživatele a prozkoumat token výjimky, abyste zjistili, jestli výjimka indikuje spolupracovné zrušení nebo nějakou jinou výjimečnou situaci.

Třída Task tímto způsobem zpracovává OperationCanceledException . Další informace naleznete v tématu Zrušení úkolu.

Naslouchání dotazováním

U dlouhotrvajících výpočtů, které smyčka nebo rekurse, můžete naslouchat žádosti o zrušení pravidelným dotazováním hodnoty CancellationToken.IsCancellationRequested vlastnosti. Pokud je truejeho hodnota , metoda by měla vyčistit a ukončit co nejrychleji. Optimální frekvence dotazování závisí na typu aplikace. Záleží na vývojáři, aby určil nejlepší frekvenci dotazování pro každý daný program. Samotné dotazování nemá významný vliv na výkon. Následující příklad ukazuje jeden možný způsob, jak se dotazovat.

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

Podrobnější příklad najdete v tématu Postupy: Naslouchání požadavkům zrušení dotazováním.

Naslouchání registrací zpětného volání

Některé operace se můžou zablokovat takovým způsobem, že nemohou včas zkontrolovat hodnotu tokenu zrušení. V těchto případech můžete zaregistrovat metodu zpětného volání, která metodu odblokuje při přijetí žádosti o zrušení.

Metoda Register vrátí CancellationTokenRegistration objekt, který se používá speciálně pro tento účel. Následující příklad ukazuje, jak pomocí Register metody zrušit asynchronní webový požadavek.

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

Objekt CancellationTokenRegistration spravuje synchronizaci vláken a zajišťuje, že zpětné volání přestane provádět v přesném časovém okamžiku.

Aby bylo možné zajistit odezvu systému a vyhnout se zablokování, musí být při registraci zpětných volání dodrženy následující pokyny:

  • Metoda zpětného volání by měla být rychlá, protože je volána synchronně, a proto volání Cancel nevrátí, dokud zpětné volání nevrátí.

  • Pokud voláte Dispose během běhu zpětného volání a držíte zámek, na kterém čeká zpětné volání, může váš program zablokovat. Po Dispose vrácení můžete uvolnit všechny prostředky vyžadované zpětným voláním.

  • Zpětná volání by neměla provádět žádné ruční vlákno ani SynchronizationContext použití v zpětném volání. Pokud zpětné volání musí běžet v určitém vlákně, použijte System.Threading.CancellationTokenRegistration konstruktor, který umožňuje určit, že cíl syncContext je aktivní SynchronizationContext.Current. Ruční podprocesování v zpětném volání může způsobit zablokování.

Podrobnější příklad najdete v tématu Postupy: Registrace zpětných volání pro žádosti o zrušení.

Naslouchání pomocí obslužného úchytu čekání

Když může zrušitelná operace blokovat, zatímco čeká na primitivní synchronizaci, jako System.Threading.ManualResetEventSystem.Threading.Semaphoreje například nebo , můžete pomocí CancellationToken.WaitHandle vlastnosti povolit operaci čekat na událost i požadavek zrušení. Obslužná rutina čekání tokenu zrušení se v reakci na požadavek zrušení signalizují a metoda může použít návratovou WaitAny hodnotu metody k určení, zda se jedná o token zrušení, který signalizoval. Operace pak může jednoduše ukončit nebo podle potřeby vyvolat .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 a System.Threading.SemaphoreSlim obě podporují rámec zrušení ve svých Wait metodách. Můžete předat CancellationToken metodu a když je požadováno zrušení, událost se probudí a vyvolá 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)

Podrobnější příklad najdete v tématu Postupy: Naslouchání požadavkům zrušení, které mají obslužné rutiny čekání.

Naslouchání více tokenům současně

V některých případech může naslouchací proces naslouchat více tokenům zrušení současně. Může být například nutné monitorovat interní token zrušení kromě tokenu předaného externě jako argument parametru metody. Chcete-li toho dosáhnout, vytvořte propojený zdroj tokenů, který může spojit dva nebo více tokenů do jednoho tokenu, jak je znázorněno v následujícím příkladu.

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

Všimněte si, že po dokončení volání zdroje propojeného tokenu je nutné volat Dispose . Podrobnější příklad najdete v tématu Postupy: Naslouchání více žádostí o zrušení.

Spolupráce mezi kódem knihovny a uživatelským kódem

Sjednocená architektura zrušení umožňuje, aby kód knihovny zrušil uživatelský kód a aby kód uživatele zrušil kooperativním způsobem. Bezproblémová spolupráce závisí na obou stranách podle těchto pokynů:

  • Pokud kód knihovny poskytuje operace s možností zrušení, měl by také poskytovat veřejné metody, které přijímají externí token zrušení, aby kód uživatele mohl požadovat zrušení.

  • Pokud kód knihovny volá uživatelský kód, měl by kód knihovny interpretovat OperationCanceledException(externalToken) jako kooperativní zrušení, a ne nutně jako výjimka selhání.

  • Delegáti uživatelů by se měli pokoušet včas reagovat na žádosti o zrušení z kódu knihovny.

System.Threading.Tasks.Task a System.Linq.ParallelEnumerable jsou to příklady tříd, které se řídí těmito pokyny. Další informace naleznete v tématu Zrušení úlohy a postupy: Zrušení plINQ dotazu.

Viz také