Lemondás felügyelt szálakban

A 4. .NET-keretrendszer kezdve a .NET egységes modellt használ az aszinkron vagy hosszú ideig futó szinkron műveletek kooperatív törléséhez. Ez a modell egy egyszerűsített, úgynevezett lemondási jogkivonaton alapul. Az az objektum, amely egy vagy több megszakítható műveletet hív meg, például új szálak vagy feladatok létrehozásával, átadja a jogkivonatot minden műveletnek. Az egyes műveletek más műveleteknek is átadhatják a jogkivonat másolatát. Egy későbbi időpontban a jogkivonatot létrehozó objektum kérheti, hogy a műveletek leállítsák azt, amit csinálnak. A lemondási kérelmet csak a kérelmező objektum bocsáthatja ki, és minden figyelő felelős azért, hogy a kérést észrevehesse, és megfelelő és időben válaszoljon rá.

A szövetkezeti lemondási modell megvalósításának általános mintája:

  • Példányosít egy CancellationTokenSource objektumot, amely kezeli és elküldi a lemondási értesítést az egyes lemondási jogkivonatoknak.

  • Adja át a CancellationTokenSource.Token tulajdonság által visszaadott jogkivonatot minden olyan tevékenységnek vagy szálnak, amely a lemondást figyeli.

  • Adjon meg egy mechanizmust minden tevékenységhez vagy szálhoz a lemondásra való reagáláshoz.

  • A lemondásról szóló értesítés megadásához hívja meg a CancellationTokenSource.Cancel metódust.

Fontos

Az CancellationTokenSource osztály megvalósítja az IDisposable felületet. Mindenképpen hívja meg a CancellationTokenSource.Dispose metódust, ha befejezte a lemondási jogkivonat forrásának használatát a nem felügyelt erőforrások felszabadításához.

Az alábbi ábrán a jogkivonat forrása és a jogkivonat összes másolata közötti kapcsolat látható.

CancellationTokenSource and cancellation tokens

A kooperatív lemondási modell megkönnyíti a lemondást támogató alkalmazások és kódtárak létrehozását, és a következő funkciókat támogatja:

  • A lemondás együttműködő, és nem kényszeríti a figyelőre. A figyelő határozza meg, hogyan lehet elegánsan leállni egy lemondási kérelemre válaszul.

  • A kérés eltér a figyeléstől. A lemondható műveletet meghívó objektumok szabályozhatják, hogy mikor (ha van ilyen) lemondást kérnek.

  • A kérést kérő objektum egyetlen metódushívással kiadja a jogkivonat összes másolatának lemondási kérését.

  • A figyelők egyszerre több jogkivonatot is meghallgathatnak, ha összekapcsolják őket egy csatolt jogkivonattal.

  • A felhasználói kód észlelheti és megválaszolhatja a kódtárkód lemondási kéréseit, a kódtárkód pedig a felhasználói kódból érkező lemondási kérelmeket és azokra reagálhat.

  • A figyelők lekérdezéssel, visszahívás-regisztrációval vagy várakozási fogópontokra való várakozással értesülhetnek a lemondási kérelmekről.

Lemondási típusok

A lemondási keretrendszer a kapcsolódó típusok halmazaként van implementálva, amelyek az alábbi táblázatban szerepelnek.

Típusnév Leírás
CancellationTokenSource Olyan objektum, amely létrehoz egy lemondási jogkivonatot, és a jogkivonat minden példányára vonatkozóan kiadja a lemondási kérelmet.
CancellationToken Egy vagy több figyelőnek átadott egyszerű értéktípus, jellemzően metódusparaméterként. A figyelők lekérdezéssel, visszahívással vagy várakozási fogóponttal figyelik a jogkivonat tulajdonságának értékét IsCancellationRequested .
OperationCanceledException A kivétel konstruktorának túlterhelései paraméterként fogadnak el.CancellationToken A figyelők igény szerint ki is adhatják ezt a kivételt a lemondás forrásának ellenőrzéséhez, és értesíthetik a többi felhasználót arról, hogy válaszolt egy lemondási kérelemre.

A lemondási modell több típusban van integrálva a .NET-be. A legfontosabbak a System.Threading.Tasks.Parallelkövetkezők, System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> és System.Linq.ParallelEnumerable. Javasoljuk, hogy ezt a kooperatív lemondási modellt használja az összes új kódtárhoz és alkalmazáskódhoz.

Példa kódra

Az alábbi példában a kérelmező objektum létrehoz egy CancellationTokenSource objektumot, majd átadja a tulajdonságát Token a megszakítható műveletnek. A kérést fogadó művelet lekérdezéssel figyeli a IsCancellationRequested jogkivonat tulajdonságának értékét. Amikor az érték válik true, a figyelő bármilyen módon leállhat. Ebben a példában a metódus csak kilép, ami sok esetben szükséges.

Feljegyzés

A példa a módszert használja annak QueueUserWorkItem bizonyítására, hogy a kooperatív lemondási keretrendszer kompatibilis az örökölt API-kkal. Az előnyben részesített System.Threading.Tasks.Task típust használó példáért tekintse meg a Feladat és gyermekei megszakítása című témakört.

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

Művelettörlés és objektumtörlés

A szövetkezeti lemondási keretrendszerben a lemondás műveletekre vonatkozik, nem objektumokra. A lemondási kérelem azt jelenti, hogy a műveletnek a lehető leghamarabb le kell állnia a szükséges törlés végrehajtása után. Egy lemondási jogkivonatnak egy "megszakítható műveletre" kell hivatkoznia, de ez a művelet implementálható a programban. IsCancellationRequested A jogkivonat tulajdonságának beállítása trueután nem állítható vissza a következőrefalse: . Ezért a lemondási jogkivonatok a lemondás után nem használhatók fel újra.

Ha objektumlemondási mechanizmust igényel, a metódus meghívásával CancellationToken.Register alapulhat a műveletlemondási mechanizmuson, ahogyan az az alábbi példában is látható.

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

Ha egy objektum egynél több egyidejű megszakítható műveletet támogat, adjon át egy külön jogkivonatot bemenetként az egyes megszakítható műveletekhez. Így az egyik művelet megszakítható anélkül, hogy a többire hatással lenne.

Lemondási kérelmek figyelése és megválaszolása

A felhasználói delegáltban a lemondható művelet megvalósítója határozza meg, hogyan lehet megszüntetni a műveletet egy lemondási kérelemre válaszul. A felhasználói meghatalmazott sok esetben egyszerűen elvégezheti a szükséges törlést, majd azonnal visszatérhet.

Összetettebb esetekben azonban előfordulhat, hogy a felhasználó meghatalmazottjának értesítenie kell a kódtár kódját a lemondásról. Ilyen esetekben a művelet leállításának helyes módja az, ha a meghatalmazott meghívja a ThrowIfCancellationRequested, metódust, ami a dobást okozza OperationCanceledException . A kódtárkód észlelheti ezt a kivételt a felhasználói delegált szálon, és megvizsgálhatja a kivétel jogkivonatát annak megállapításához, hogy a kivétel kooperatív lemondást vagy más kivételes helyzetet jelez-e.

Az Task osztály így kezeli a fogópontokat OperationCanceledException . További információ: Tevékenység lemondása.

Figyelés lekérdezéssel

Az ismétlődő vagy ismétlődő hosszú ideig futó számítások esetében a lemondási kéréseket figyelheti a tulajdonság értékének CancellationToken.IsCancellationRequested rendszeres lekérdezésével. Ha az értéke, truea metódusnak a lehető leggyorsabban el kell törölnie és leállnia. A lekérdezés optimális gyakorisága az alkalmazás típusától függ. A fejlesztő feladata, hogy meghatározza az adott programhoz tartozó legjobb lekérdezési gyakoriságot. A lekérdezés önmagában nem befolyásolja jelentősen a teljesítményt. Az alábbi példa egy lehetséges lekérdezési módot mutat be.

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

Egy teljesebb példa : A lemondási kérelmek figyelése lekérdezéssel.

Figyelés visszahívás regisztrálásával

Egyes műveletek olyan módon blokkolhatók, hogy nem tudják időben ellenőrizni a lemondási jogkivonat értékét. Ezekben az esetekben regisztrálhat egy visszahívási metódust, amely feloldja a letiltást a lemondási kérelem fogadásakor.

A Register metódus egy CancellationTokenRegistration kifejezetten erre a célra használt objektumot ad vissza. Az alábbi példa bemutatja, hogyan szakíthat meg aszinkron webkérelmet a Register metódussal.

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

Az CancellationTokenRegistration objektum kezeli a szálszinkronizálást, és gondoskodik arról, hogy a visszahívás egy adott időpontban leálljon.

A rendszer válaszkészségének biztosítása és a holtpontok elkerülése érdekében a visszahívások regisztrálásakor a következő irányelveket kell követni:

  • A visszahívási módszernek gyorsnak kell lennie, mert szinkron módon van meghívva, ezért a hívás Cancel nem tér vissza, amíg a visszahívás vissza nem tér.

  • Ha a visszahívás futtatása közben hív Dispose , és a visszahívásra várakozó zárolást tart, a program holtpontra állíthatja. A visszatérés után Dispose felszabadíthatja a visszahíváshoz szükséges erőforrásokat.

  • A visszahívások nem végezhetnek manuális szálat vagy SynchronizationContext használatot a visszahívásokban. Ha egy visszahívásnak egy adott szálon kell futnia, használja a System.Threading.CancellationTokenRegistration konstruktort, amely lehetővé teszi annak megadását, hogy a cél syncContext az aktív SynchronizationContext.Current. Ha manuális szálkezelést végez egy visszahívásban, az holtpontot okozhat.

További teljesebb példa : Visszahívások regisztrálása lemondási kérelmekhez.

Figyelés várakozási fogópont használatával

Ha egy lemondható művelet blokkolhatja, amíg egy szinkronizálási primitíven( például egy System.Threading.ManualResetEvent vagy System.Threading.Semaphore) várakozik, a tulajdonság használatával CancellationToken.WaitHandle engedélyezheti, hogy a művelet az eseményre és a lemondási kérelemre is várakozhasson. A lemondási jogkivonat várakozási leírója egy lemondási kérelemre válaszként lesz jelezve, és a metódus a metódus visszatérési értékével WaitAny állapíthatja meg, hogy a lemondási jogkivonat volt-e a jelzés. A művelet ezután egyszerűen kiléphet, vagy szükség szerint dobhat egy OperationCanceledExceptionműveletet.

// 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 és System.Threading.SemaphoreSlim mindkettő támogatja a lemondási keretrendszert a módszereikben Wait . Átadhatja a CancellationToken metódusnak, és amikor a lemondást kéri, az esemény felébred, és eldob egy 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)

Egy teljesebb példa : Útmutató: Várakozási fogópontokkal rendelkező lemondási kérelmek figyelése.

Több jogkivonat egyidejű hallgatása

Bizonyos esetekben előfordulhat, hogy a figyelőnek egyszerre több lemondási jogkivonatot is meg kell hallgatnia. Előfordulhat például, hogy egy megszakítható műveletnek egy belső lemondási jogkivonatot kell figyelnie a külsőleg átadott jogkivonat mellett egy metódusparaméter argumentumaként. Ehhez hozzon létre egy csatolt jogkivonat-forrást, amely két vagy több jogkivonatot csatlakoztathat egy jogkivonathoz, ahogyan az az alábbi példában látható.

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

Figyelje meg, hogy a csatolt jogkivonat forrását akkor kell meghívnia Dispose , ha végzett vele. Egy teljesebb példa : Útmutató: Több lemondási kérelem figyelése.

Együttműködés a kódtárkód és a felhasználói kód között

Az egységes lemondási keretrendszer lehetővé teszi, hogy a kódtárkód visszavonja a felhasználói kódot, a felhasználói kód pedig együttműködési módon mondja le a kódtár kódját. A zökkenőmentes együttműködés az alábbi irányelveket követve minden oldaltól függ:

  • Ha a kódtárkód megszakítható műveleteket biztosít, a külső lemondási jogkivonatot elfogadó nyilvános metódusokat is meg kell adnia, hogy a felhasználói kód igényelhesse a lemondást.

  • Ha a kódtárkód felhasználói kódba hív be, a kódtárkódnak az OperationCanceledException(externalToken) függvényt kooperatív lemondásként kell értelmeznie, és nem feltétlenül hibakivételként.

  • A felhasználói meghatalmazottaknak időben meg kell kísérelni a kódtárkódból érkező lemondási kérelmeket.

System.Threading.Tasks.Task és System.Linq.ParallelEnumerable példák azokra az osztályokra, amelyek az alábbi irányelveket követik. További információ: Feladattörlés és útmutató: PLINQ-lekérdezés lemondása.

Lásd még