Abbruch in verwalteten Threads

Ab .NET Framework 4 verwendet .NET ein einheitliches Modell für den kooperativen Abbruch von asynchronen oder lang andauernden synchronen Vorgängen. Dieses Modell basiert auf einem einfachen Objekt, dem sogenannten "Abbruchtoken". Das Objekt, das einen oder mehrere abbrechbare Vorgänge aufruft, z. B. durch Erstellen neuer Threads oder Aufgaben, übergibt das Token an jeden Vorgang. Einzelne Vorgänge können wiederum Kopien des Tokens an andere Vorgänge übergeben. Zu einem späteren Zeitpunkt kann das Objekt, das das Token erstellt hat, damit anfordern, dass die Vorgänge ihre aktuelle Aktivität einstellen. Nur das anfordernde Objekt kann die Abbruchanforderung ausgeben, und jeder Listener ist dafür verantwortlich, die Anforderung zu bemerken und angemessen und rechtzeitig darauf zu reagieren.

Das allgemeine Muster für die Implementierung des kooperativen Abbruchmodells lautet:

  • Instanziieren Sie ein CancellationTokenSource-Objekt, das die Abbruchbenachrichtigung verwaltet und an die einzelnen Abbruchtoken sendet.

  • Übergeben Sie das zurückgegebene Token über die CancellationTokenSource.Token-Eigenschaft an jede Aufgabe oder Thread, der zum Lauschen verwendet wird, um den Abbruch zu bemerken.

  • Stellen Sie einen Mechanismus für jede Aufgabe oder Thread bereit, um auf den Abbruch zu reagieren.

  • Rufen Sie die CancellationTokenSource.Cancel-Methode auf, um eine Benachrichtigung über den Abbruch bereitzustellen.

Wichtig

Die CancellationTokenSource-Klasse implementiert die IDisposable-Schnittstelle. Sie sollten darauf achten, die CancellationTokenSource.Dispose-Methode aufzurufen, wenn Sie die Verwendung der Abbruchtokenquelle abgeschlossen haben, um alle darin enthaltenen, nicht verwalteten Ressourcen freizugeben.

Die folgende Abbildung zeigt die Beziehung zwischen einer Tokenquelle und allen Kopien des Tokens.

CancellationTokenSource and cancellation tokens

Das kooperative Abbruchmodell vereinfacht die Erstellung von abbruchfähigen Anwendungen sowie Bibliotheken und unterstützt die folgenden Features:

  • Der Abbruch ist kooperativ und wird für den Listener nicht erzwungen. Der Listener bestimmt, wie die ordnungsgemäße Beendigung als Reaktion auf eine Abbruchanforderung durchgeführt wird.

  • Die Anforderung unterscheidet sich vom Lauschvorgang. Ein Objekt, das einen abbrechbaren Vorgang aufruft, kann steuern, wann (falls überhaupt) der Abbruch angefordert wird.

  • Das anfordernde Objekt sendet die Abbruchanforderung mithilfe eines einzigen Methodenaufrufs an alle Kopien des Tokens.

  • Ein Listener kann mehrere Token gleichzeitig belauschen, indem diese zu einem verknüpften Token verbunden werden.

  • Benutzercode kann Abbruchanforderungen aus Bibliothekscode erkennen und auf diese reagieren, während Bibliothekscode Abbruchanforderungen aus Benutzercode erkennen und auf diese reagieren kann.

  • Listener können durch Abruf, Rückrufregistrierung oder Warten auf Wait-Handles über Abbruchanforderungen benachrichtigt werden.

Abbruchtypen

Das Abbruchframework ist als Gruppe von verwandten Typen implementiert, die in der folgenden Tabelle aufgeführt sind.

Typname Beschreibung
CancellationTokenSource Ein Objekt, das ein Abbruchtoken erstellt und auch die Abbruchanforderung für alle Kopien dieses Token ausgibt.
CancellationToken Ein einfacher Werttyp, der in der Regel als Methodenparameter an mindestens einen Listener übergeben wird. Listener überwachen den Wert der IsCancellationRequested-Eigenschaft des Token durch Abruf, Rückruf oder Wait-Handle.
OperationCanceledException Überladungen des Konstruktors dieser Ausnahme akzeptieren ein CancellationToken als Parameter. Listener können diese Ausnahme optional auslösen, um die Quelle des Abbruchs zu überprüfen und andere darüber zu benachrichtigen, dass auf eine Abbruchanforderung reagiert wurde.

Das Abbruchmodell ist in .NET in mehreren Typen integriert. Die wichtigsten sind System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult> und System.Linq.ParallelEnumerable. Es wird empfohlen, dieses kooperative Abbruchmodell für sämtlichen neuen Bibliotheks- und Anwendungscode zu verwenden.

Codebeispiel

Im folgenden Beispiel erstellt das anfordernde Objekt ein CancellationTokenSource-Objekt und übergibt dann seine Token-Eigenschaft an den abbrechbaren Vorgang. Der Vorgang, der die Anforderung empfängt, überwacht den Wert von der IsCancellationRequested-Eigenschaft des Token durch Abruf. Wenn der Wert zu true wechselt, kann der Listener auf geeignete Weise beendet werden. In diesem Beispiel wird die Methode einfach beendet und das ist auch häufig alles, was erforderlich ist.

Hinweis

Im Beispiel wird die QueueUserWorkItem-Methode verwendet, um zu veranschaulichen, dass das kooperative Abbruchframework mit Legacy-APIs kompatibel ist. Ein Beispiel, das den neuen bevorzugten System.Threading.Tasks.Task-Typ verwendet, finden Sie unter Vorgehensweise: Abbrechen einer Aufgabe und ihrer untergeordneten Elemente.

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

Vorgangsabbruch im Vergleich zum Objektabbruch

Im kooperativen Abbruchframework bezieht sich der Abbruch auf Vorgänge und nicht auf Objekte. Die Abbruchanforderung bedeutet, dass der Vorgang so schnell wie möglich beendet werden soll, nachdem alle erforderlichen Bereinigungen ausgeführt wurden. Ein Abbruchtoken sollte auf einen "abbrechbaren Vorgang" verweisen, aber dieser Vorgang kann in Ihrem Programm implementiert werden. Nachdem die IsCancellationRequested-Eigenschaft des Tokens auf true festgelegt wurde, kann sie nicht wieder auf false zurückgesetzt werden. Daher können Abbruchtoken nicht wiederverwendet werden, nachdem sie abgebrochen wurden.

Wenn Sie einen Objektabbruchmechanismus benötigen, können Sie ihn durch Aufrufen der CancellationToken.Register-Methode auf dem Vorgangsabbruchmechanismus basieren lassen, wie im folgenden Beispiel gezeigt.

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

Wenn ein Objekt mehrere gleichzeitig abbrechbare Vorgänge unterstützt, übergeben Sie ein separates Token als Eingabe für die einzelnen abbrechbaren Vorgänge. Auf diese Weise kann ein Vorgang ohne Auswirkung auf die anderen abgebrochen werden.

Lauschen und Reagieren auf Abbruchanforderungen

Im Benutzerdelegaten bestimmt der Implementierer eines abbrechbaren Vorgangs, wie der Vorgang als Reaktion auf eine Abbruchanforderung beendet wird. In vielen Fällen kann der Benutzerdelegat einfach die erforderliche Bereinigung ausführen und dann sofort zurückkehren.

In komplexeren Fällen ist es für den Benutzerdelegaten möglicherweise erforderlich, den Bibliothekscode darüber zu benachrichtigen, dass der Abbruch aufgetreten ist. In solchen Fällen besteht für den Delegaten die richtige Methode zum Beenden des Vorgangs darin, die ThrowIfCancellationRequested-Methode aufzurufen, die zum Auslösen einer OperationCanceledException führt. Bibliothekscode kann diese Ausnahme im Benutzerdelegatthread abfangen und das Token der Ausnahme untersuchen, um zu ermitteln, ob die Ausnahme auf einen kooperativen Abbruch oder eine andere Ausnahmesituation hinweist.

Die Task-Klasse behandelt OperationCanceledException auf diese Weise. Weitere Informationen finden Sie unter Aufgabenabbruch.

Lauschen durch Abruf

Bei langandauernden Berechnungen mit Schleifen oder Rekursionen können Sie auf eine Abbruchanforderung lauschen, indem Sie den Wert der CancellationToken.IsCancellationRequested-Eigenschaft in regelmäßigen Abständen abfragen. Wenn der Wert true lautet, sollte die Methode die Bereinigung vornehmen und so schnell wie möglich beendet werden. Die optimale Häufigkeit für das Abrufen hängt vom Typ der Anwendung ab. Es ist Aufgabe des Entwicklers, die beste Abrufhäufigkeit für ein Programm zu ermitteln. Der Abruf selbst hat keinen signifikanten Einfluss auf die Leistung. Im folgenden Beispiel wird eine Möglichkeit für den Abruf veranschaulicht.

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

Ein ausführlicheres Beispiel finden Sie unter Vorgehensweise: Lauschen auf Abbruchanforderungen durch Abruf.

Lauschen durch Registrieren eines Rückrufs

Einige Vorgänge können so blockiert werden, dass sie den Wert des Abbruchtokens nicht rechtzeitig überprüfen können. In diesen Fällen können Sie eine Rückrufmethode registrieren, die die Blockierung der Methode aufhebt, wenn eine Abbruchanforderung empfangen wird.

Die Register-Methode gibt ein CancellationTokenRegistration-Objekt zurück, das speziell für diesen Zweck verwendet wird. Das folgende Beispiel zeigt, wie Sie die Register-Methode zum Abbrechen einer asynchronen Webanforderung verwenden.

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

Das CancellationTokenRegistration-Objekt verwaltet die Threadsynchronisierung und stellt sicher, dass die Ausführung des Rückrufs zu einem bestimmten Zeitpunkt beendet wird.

Um die Reaktionsfähigkeit des Systems sicherzustellen und Deadlocks zu vermeiden, müssen die folgenden Richtlinien beim Registrieren von Rückrufen beachtet werden:

  • Die Rückrufmethode muss schnell sein, da sie synchron aufgerufen wird und der Aufruf von Cancel daher nicht zurückgegeben wird, bevor der Rückruf zurückgegeben wurde.

  • Wenn Sie Dispose aufrufen, während der Rückruf ausgeführt wird, und Sie eine Sperre aufrechterhalten, auf die der Rückruf wartet, kann für das Programm ein Deadlock auftreten. Nachdem Dispose zurückgegeben wurde, können Sie alle für den Rückruf erforderlichen Ressourcen freigeben.

  • Rückrufe sollten keine manuellen Threads durchführen oder SynchronizationContext in einem Rückruf verwenden. Wenn ein Rückruf für einen bestimmten Thread ausgeführt werden muss, verwenden Sie den System.Threading.CancellationTokenRegistration-Konstruktor, mit dem Sie angeben können, dass der Zielsynchronisierungskontext das aktive SynchronizationContext.Current ist. Manuelles Threading in einem Rückruf kann zu einem Deadlock führen.

Ein ausführlicheres Beispiel finden Sie unter Vorgehensweise: Registrieren von Rückrufen für Abbruchanforderungen.

Lauschen mithilfe eines Wait-Handles

Wenn ein abbrechbarer Vorgang blockiert werden kann, während er auf einen primitiven Synchronisierungstyp wie System.Threading.ManualResetEvent oder System.Threading.Semaphore wartet, können Sie die CancellationToken.WaitHandle-Eigenschaft verwenden, um es dem Vorgang zu ermöglichen, auf das Ereignis und die Abbruchanforderung zu warten. Das Wait-Handle des Abbruchtokens wird als Reaktion auf eine Abbruchanforderung signalisiert, und die Methode kann anhand des Rückgabewerts der WaitAny-Methode bestimmen, ob das Abbruchtoken signalisiert hat. Der Vorgang kann dann einfach enden oder ggf. eine OperationCanceledException auslösen.

// 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))

Sowohl System.Threading.ManualResetEventSlim als auch System.Threading.SemaphoreSlim unterstützen das Abbruchframework in ihren Wait-Methoden. Sie können das CancellationToken an die Methode übergeben und bei der Abbruchanforderung wird das Ereignis reaktiviert, das ein OperationCanceledException auslöst.

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)

Ein ausführlicheres Beispiel finden Sie unter Vorgehensweise: Lauschen auf Abbruchanforderungen mit Wait-Handles.

Gleichzeitiges Lauschen auf mehrere Token

In einigen Fällen muss ein Listener möglicherweise auf mehrere Abbruchtoken gleichzeitig lauschen. Ein abbrechbarer Vorgang muss z. B. möglicherweise zusätzlich zu einem extern als Argument an einen Methodenparameter übergebenen Token ein internes Abbruchtoken überwachen. Zu diesem Zweck erstellen Sie eine verknüpfte Tokenquelle, die zwei oder mehr Token zu einem Token verbinden kann, wie im folgenden Beispiel gezeigt.

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

Beachten Sie, dass Sie Dispose für die verknüpfte Tokenquelle aufrufen müssen, wenn Sie damit fertig sind. Ein ausführlicheres Beispiel finden Sie unter Vorgehensweise: Lauschen auf mehrere Abbruchanforderungen.

Kooperation zwischen Bibliothekscode und Benutzercode

Das einheitliche Abbruchframework ermöglicht es dem Bibliothekscode, den Benutzercode abzubrechen. Ebenso ermöglicht es dem Benutzercode, den Bibliothekscode auf kooperative Weise abzubrechen. Die problemlose Zusammenarbeit hängt davon ab, ob die beiden Seiten die folgenden Richtlinien beachten:

  • Wenn der Bibliothekscode abbrechbare Vorgänge bereitstellt, sollten auch öffentliche Methoden bereitgestellt werden, die ein externes Abbruchtoken akzeptieren, damit der Benutzercode den Abbruch anfordern kann.

  • Wenn der Bibliothekscode einen Aufruf innerhalb des Benutzercodes ausführt, sollte der Bibliothekscode ein OperationCanceledException(externalToken) als kooperativen Abbruch und nicht notwendigerweise als Fehlerausnahme interpretieren.

  • Benutzerdelegaten sollten versuchen, zeitnah auf Abbruchanforderungen aus Bibliothekscode zu reagieren.

System.Threading.Tasks.Task und System.Linq.ParallelEnumerable sind Beispiele für Klassen, die diese Richtlinien einhalten. Weitere Informationen finden Sie unter Aufgabenabbruch und Vorgehensweise: Abbrechen einer PLINQ-Abfrage.

Siehe auch