Sdílet prostřednictvím


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ém 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 mohou 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ý posluchač zodpovídá za zaznamenání požadavku a reagování na něj vhodný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 pro zrušení.

  • Předejte token vrácený vlastností CancellationTokenSource.Token každému ú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 a zadejte oznámení o zrušení.

Důležité

Třída CancellationTokenSource implementuje rozhraní IDisposable. Po dokončení použití zdroje tokenu zrušení byste měli zavolat metodu CancellationTokenSource.Dispose, aby se uvolnily všechny nespravované prostředky, které obsahuje.

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

CancellationTokenSource a tokeny zrušení

Model kooperativního zrušení zjednodušuje vytváření aplikací a knihoven umožňujících zrušení a zahrnuje následující funkce:

  • Zrušení je sjednané a není vnucováno posluchači. Posluchač 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.

  • Objekt, který podává žádost, vydá žádost o zrušení pro všechny kopie tokenu prostřednictvím jediného volání metody.

  • Posluchač může poslouchat více tokenů zároveň tím, že je spojí do jednoho propojeného tokenu.

  • Uživatelský kód může zaznamenat a reagovat na žádosti o zrušení z kódu knihovny, stejně jako kód knihovny může zaznamenat a reagovat na žádosti o zrušení z uživatelského kódu.

  • Posluchači mohou být upozorněni na žádosti o zrušení dotazováním, registrací zpětného volání nebo čekáním na synchronizační objekty.

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 Lehký typ hodnoty, který je předáván jednomu nebo více posluchačům, obvykle jako parametr metody. Posluchače monitorují hodnotu vlastnosti IsCancellationRequested tokenu průzkumem, zpětným voláním nebo pomocí popisovače čekání.
OperationCanceledException Přetížení konstruktoru této výjimky přijímají CancellationToken jako parametr. Nasluchače můžou volitelně vyvolat tuto výjimku, aby ověřili příčinu zrušení a informovali 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.Task, System.Threading.Tasks.Task<TResult> a System.Linq.ParallelEnumerable. Doporučujeme používat tento model kooperativního rušení pro všechny nové knihovny a aplikační kód.

Příklad kódu

V následujícím příkladu vytvoří žádající objekt CancellationTokenSource objekt a pak předá jeho Token vlastnost do zrušitelné operace. Operace, která obdrží požadavek, monitoruje hodnotu vlastnosti IsCancellationRequested 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 jednoduše 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á preferovaný typ System.Threading.Tasks.Task, najdete v části Postupy: Jak zrušit úkol a jeho podřízené úkoly.

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

Zrušení operace versus zrušení objektu

V rámci rámce spolupráce se termín "zrušení" vztahuje 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 odpovídat jedné "zrušitelné operaci", avšak tato operace může být implementována ve vašem programu. Jakmile je vlastnost IsCancellationRequested tokenu nastavená na true, nedá se obnovit na false. Proto po zrušení nelze znovu použít tokeny zrušení.

Pokud potřebujete mechanismus zrušení objektu, můžete ho založit na mechanismu zrušení operace voláním CancellationToken.Register metoda, 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 {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

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ým způsobem ukončení operace, když delegát zavolá metodu ThrowIfCancellationRequested, což způsobí vyvolání OperationCanceledException. 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 části Zrušení úkolu.

Naslouchání prostřednictvím průzkumu

Pro dlouhotrvající výpočty, které se opakují nebo rekurzivně volají, můžete naslouchat žádosti o zrušení pravidelným dotazováním hodnoty vlastnosti CancellationToken.IsCancellationRequested. Pokud je její hodnota true, měla by metoda co nejrychleji provést úklid a ukončit. 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 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

Pro podrobnější příklad, podívejte se na Postup: Poslech požadavků na zrušení pomocí dotazování.

Naslouchání pomocí registrace 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í objekt CancellationTokenRegistration, který se používá speciálně pro tento účel. Následující příklad ukazuje, jak pomocí metody Register zrušit asynchronní webový požadavek.

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

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 se volání na Cancel nevrátí, dokud se zpětné volání nevrátí.

  • Pokud voláte Dispose, když je zpětné volání spuštěné a držíte zámek, na kterém zpětné volání čeká, může váš program zablokovat. Po návratu z Dispose můžete uvolnit všechny prostředky vyžadované callbackem.

  • Zpětná volání by neměla provádět žádné ručně spravované vlákno ani použití SynchronizationContext v rámci zpětného volání. Pokud musí callback běžet v určitém vlákně, použijte konstruktor System.Threading.CancellationTokenRegistration, který umožňuje určit, že cílový synchronizační kontext 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 Jak na to: Registrace callbacků pro žádosti o zrušení.

Naslouchání s použitím čekacího úchopu

Když může zrušitelná operace blokovat, zatímco čeká na synchronizační primitivum, jako je System.Threading.ManualResetEvent nebo System.Threading.Semaphore, můžete pomocí vlastnosti CancellationToken.WaitHandle umožnit operaci čekat jak na událost, tak na žádost o zrušení. Čekací rukojeť tokenu zrušení bude signalizována ve výsledku požadavku na zrušení, a metoda může použít návratovou hodnotu metody WaitAny k určení, zda to byl token zrušení, kdo způsobil signalizaci. Operace pak může jednoduše skončit nebo podle potřeby vyvolat chybu 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 podporují rámec pro zrušení ve svých metodách Wait. Můžete předat CancellationToken této metodě a když je požadováno zrušení, událost se aktivuje 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 Jak naslouchat požadavkům na zrušení, které mají čekací signály.

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

V některých případech může posluchač muset 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 musíte zavolat Dispose na propojeném zdroji tokenu, jakmile s ním skončíte. Podrobnější příklad najdete v tématu Jak na to: jak naslouchat více žádostem o zrušení.

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

Sjednocený rámec zrušení umožňuje, aby kód knihovny mohl zrušit uživatelský kód a uživatelský kód mohl zrušit kód knihovny, to vše ve spolupracujícím režimu. 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 příklady tříd, které dodržují tyto pokyny. Další informace naleznete v tématu Zrušení úlohy a Jak na to: Zrušení dotazu PLINQ.

Viz také