Abbruch

In .NET Framework, Version 4 wird ein neues einheitliches Modell für den kooperativen Abbruch asynchroner Vorgänge oder Vorgänge mit langer Laufzeit eingeführt. Dieses Modell basiert auf einem kompakten Objekt, das als Abbruchtoken bezeichnet wird. Das Objekt, das einen abbrechbaren Vorgang aufruft, z. B. durch Erstellen eines neuen Threads oder einer Aufgabe, übergibt das Token an den Vorgang. Dieser Vorgang kann wiederum Kopien des Tokens an andere Vorgänge übergeben. Später kann das Objekt, das das Token erstellt hat, dieses verwenden, um anzufordern, dass der Vorgang seine Aufgabe beendet. Nur das anfordernde Objekt kann die Abbruchanforderung senden, und jeder Listener ist dafür verantwortlich, die Anforderung zu bemerken und rechtzeitig darauf zu reagieren. Die folgende Abbildung zeigt die Beziehung zwischen einer Tokenquelle und allen Kopien des Tokens.

CancellationTokenSource und CancellationTokens

Das neue Abbruchmodell macht es einfacher, abbruchaktivierte Anwendungen und Bibliotheken zu erstellen, und unterstützt die folgenden Funktionen:

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

  • Die Anforderung ist nicht mit einem Lauschen identisch. Ein Objekt, das einen abbrechbaren Vorgang aufruft, kann steuern, wann der Abbruch ggf. angefordert wird.

  • Das anfordernde Objekt sendet mit nur einem Methodenaufruf die Abbruchanforderung an alle Kopien des Tokens.

  • Ein Listener kann gleichzeitig auf mehrere Token lauschen, indem er sie in einem verknüpften Token zusammenführt.

  • Benutzercode kann Abbruchanforderungen aus Bibliothekscode erkennen und darauf reagieren, und Bibliothekscode kann Abbruchanforderungen aus Benutzercode erkennen und darauf reagieren.

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

Neue Abbruchtypen

Das neue Abbruchframework wird als ein Satz verwandter Typen implementiert, die in der folgenden Tabelle aufgeführt werden.

Typname

Beschreibung

CancellationTokenSource

Objekt, das ein Abbruchtoken erstellt und die Abbruchanforderung für alle Kopien dieses Tokens sendet.

CancellationToken

Einfacher Werttyp, der an einen oder mehrere Listener übergeben wird, in der Regel als Methodenparameter. Listener überwachen den Wert der IsCancellationRequested-Eigenschaft des Tokens durch Abruf, Rückruf oder Wait-Handle.

OperationCanceledException

Neue Überladungen dieser Ausnahme nehmen ein CancellationToken als Eingabeparameter an. Listener können diese Ausnahme optional auslösen, um die Quelle des Abbruchs zu überprüfen und andere zu benachrichtigen, dass auf eine Abbruchanforderung reagiert wurde.

Das neue Abbruchmodell wird in mehreren Typen in .NET Framework integriert. Die wichtigsten sind System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult> und System.Linq.ParallelEnumerable. Es wird empfohlen, dass Sie dieses neue Abbruchmodell für den gesamten neuen Bibliotheks- und Anwendungscode 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 der IsCancellationRequested-Eigenschaft des Tokens durch Abruf. Wenn der Wert true wird, kann der Listener auf jede geeignete Art beendet werden. In diesem Beispiel wird die Methode einfach beendet. In vielen Fällen ist dies ausreichend.

HinweisHinweis

Im Beispiel wird anhand der QueueUserWorkItem-Methode veranschaulicht, dass das neue Abbruchframework mit Legacy-APIs kompatibel ist.Ein Beispiel, in dem der neue, bevorzugte System.Threading.Tasks.Task-Typ verwendet wird, finden Sie unter Gewusst wie: Abbrechen einer Aufgabe und ihrer untergeordneten Elemente.

Shared Sub CancelWithThreadPoolMiniSnippet()


    'Thread 1: The Requestor
    ' Create the token source.
    Dim cts As New CancellationTokenSource()

    ' Pass the token to the cancelable operation.
    ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)

    ' Request cancellation by setting a flag on the token.
    cts.Cancel()
    ' end block
End Sub

'Thread 2: The Listener
Shared Sub DoSomeWork(ByVal obj As Object)

    Dim token As CancellationToken = CType(obj, CancellationToken)
    For i As Integer = 0 To 1000000

        ' Simulating work.
        Thread.SpinWait(5000000)

        If token.IsCancellationRequested Then

            ' Perform cleanup if necessary.
            '...
            ' Terminate the operation.
            Exit For
        End If
    Next
End Sub
static void CancelWithThreadPoolMiniSnippet()
{

    //Thread 1: The Requestor
    // Create the token source.
    CancellationTokenSource cts = new CancellationTokenSource();

    // Pass the token to the cancelable operation.
    ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);

    // Request cancellation by setting a flag on the token.
    cts.Cancel();
}

//Thread 2: The Listener
static void DoSomeWork(object obj)
{
    CancellationToken token = (CancellationToken)obj;
    for (int i = 0; i < 100000; i++)
    {
        // Simulating work.
        Thread.SpinWait(5000000);

        if (token.IsCancellationRequested)
        {
            // Perform cleanup if necessary.
            //...
            // Terminate the operation.
            break;
        }
    }
}

Vorgangsabbruch und Objektabbruch

Im neuen Abbruchframework bezieht sich Abbruch auf Vorgänge und nicht auf Objekte. Die Abbruchanforderung bedeutet, dass der Vorgang so schnell wie möglich beendet werden soll, nachdem ggf. erforderliche Bereinigungen ausgeführt wurden. Ein Abbruchtoken sollte auf einen "abbrechbaren Vorgang" verweisen, dieser Vorgang ist jedoch möglicherweise im Programm implementiert. Nachdem die IsCancellationRequested-Eigenschaft des Tokens auf true festgelegt wurde, kann sie nicht 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 als Grundlage den Vorgangsabbruchmechanismus verwenden, wie im folgenden Beispiel dargestellt.

Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token

' User defined Class with its own method for cancellation
Dim obj1 As New MyCancelableObject()
Dim obj2 As New MyCancelableObject()
Dim obj3 As New MyCancelableObject()

' 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()
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

// User defined Class with its own method for cancellation
var obj1 = new MyCancelableObject();
var obj2 = new MyCancelableObject();
var obj3 = new MyCancelableObject();

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

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

Lauschen und Reagieren auf Abbruchanforderungen

Der Implementierer eines abbrechbaren Vorgangs bestimmt im Benutzerdelegaten, 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 kann es jedoch für den Benutzerdelegaten erforderlich sein, Bibliothekscode zu benachrichtigen, dass ein Abbruch erfolgt ist. In solchen Fällen besteht das korrekte Verfahren zum Beenden des Vorgangs darin, dass der Delegat ThrowIfCancellationRequested() aufruft. Hierdurch wird eine OperationCanceledException ausgelöst. Neue Überladungen dieser Ausnahme in .NET Framework, Version 4 nehmen ein CancellationToken als Argument an. In Bibliothekscode kann diese Ausnahme im Benutzerdelegatthread abgefangen und das Token der Ausnahme untersucht werden, um zu bestimmen, ob die Ausnahme einen kooperativen Abbruch oder eine andere Ausnahmesituation angibt.

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

Lauschen durch Abruf

Bei Berechnungen mit langer Laufzeit, die Schleifen oder Rekursionen aufweisen, können Sie auf eine Abbruchanforderung lauschen, indem Sie regelmäßig den Wert der CancellationToken.IsCancellationRequested-Eigenschaft abrufen. Wenn der Wert true lautet, sollte die Methode so schnell wie möglich die Bereinigung und das Beenden ausführen. Die optimale Häufigkeit für das Abrufen ist abhängig vom Typ der Anwendung. Der Entwickler muss die beste Abrufhäufigkeit für ein Programm bestimmen. Das Abrufen selbst hat keine signifikanten Auswirkungen auf die Leistung. Das folgende Beispiel zeigt eine Möglichkeit zum Abrufen.

    Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)

        For x As Integer = 0 To rect.columns

            For y As Integer = 0 To rect.rows

                ' Simulating work.
                Thread.SpinWait(5000)
                Console.Write("0' end block,1' end block ", x, y)
            Next

            ' Assume that we know that the inner loop is very fast.
            ' Therefore, checking once per row is sufficient.
            If token.IsCancellationRequested = True Then

                ' Cleanup or undo here if necessary...
                Console.WriteLine("\r\nCancelling after row 0' end block.", x)
                Console.WriteLine("Press any key to exit.")
                ' then...
                Exit For
                ' ...or, if using Task:
                ' token.ThrowIfCancellationRequested()
            End If
        Next
    End Sub

static void NestedLoops(Rectangle rect, CancellationToken token)
{
    for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++)
    {
        for (int y = 0; y < rect.rows; y++)
        {
            // Simulating work.
            Thread.SpinWait(5000);
            Console.Write("{0},{1} ", x, y);
        }

        // Assume that we know that the inner loop is very fast.
        // Therefore, checking once per row is sufficient.
        if (token.IsCancellationRequested)
        {
            // Cleanup or undo here if necessary...
            Console.WriteLine("\r\nCancelling after row {0}.", x);
            Console.WriteLine("Press any key to exit.");
            // then...
            break;
            // ...or, if using Task:
            // token.ThrowIfCancellationRequested();
        }
    }
}

Ein ausführlicheres Beispiel finden Sie unter Gewusst wie: 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. Für diese Fälle 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 explizit für diesen Zweck verwendet wird. Im folgenden Beispiel wird gezeigt, wie mit der Register-Methode eine asynchrone Webanforderung abgebrochen wird.

Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
Dim wc As New WebClient()

' To request cancellation on the token
' will call CancelAsync on the WebClient.
token.Register(Sub() wc.CancelAsync())

Console.WriteLine("Starting request")
wc.DownloadStringAsync(New Uri("https://www.contoso.com"))
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;            
WebClient wc = new WebClient();

// To request cancellation on the token
// will call CancelAsync on the WebClient.
token.Register(() => wc.CancelAsync());

Console.WriteLine("Starting request");
wc.DownloadStringAsync(new Uri("https://www.contoso.com"));

Das CancellationTokenRegistration-Objekt verwaltet die Threadsynchronisierung und stellt sicher, dass der Rückruf zu einem genauen Zeitpunkt beendet wird.

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

  • Die Rückrufmethode muss schnell sein, da sie synchron aufgerufen wird und der Aufruf von Cancel daher erst beendet wird, wenn der Rückruf beendet wurde.

  • Wenn Sie Dispose während des Rückrufs aufrufen und eine Sperre aktivieren, auf die der Rückruf wartet, kann im Programm ein Deadlock auftreten. Nachdem Dispose beendet wurde, können Sie für den Rückruf erforderliche Ressourcen freigeben.

  • Für Rückrufe sollten weder manuelle Threads noch SynchronizationContext verwendet werden. Wenn ein Rückruf für einen bestimmten Thread ausgeführt werden muss, verwenden Sie den System.Threading.CancellationTokenRegistration-Konstruktor, mit dem Sie den Ziel-syncContext als aktiven SynchronizationContext.Current festlegen können. Manuelles Threading kann in einem Rückruf einen Deadlock verursachen.

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

Lauschen mithilfe eines Wait-Handles

Wenn ein abbrechbarer Vorgang blockiert werden kann, während er auf einen Synchronisierungsprimitiven wartet, z. B. ein System.Threading.ManualResetEvent oder System.Threading.Semaphore, können Sie mit der CancellationToken.WaitHandle-Eigenschaft für den Vorgang aktivieren, dass er auf das Ereignis und die Abbruchanforderung wartet. 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 die Signalisierung durch das Abbruchtoken erfolgt ist. Der Vorgang kann dann einfach beendet werden oder ggf. eine OperationCanceledException auslösen.

' Wait on the event if it is not signaled.
Dim myWaitHandle(2) As WaitHandle
myWaitHandle(0) = mre
myWaitHandle(1) = token.WaitHandle
Dim eventThatSignaledIndex =
    WaitHandle.WaitAny(myWaitHandle, _
                        New TimeSpan(0, 0, 20))
// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
    WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                        new TimeSpan(0, 0, 20));

In neuem Code, der auf .NET Framework, Version 4 abzielt, unterstützen System.Threading.ManualResetEventSlim und System.Threading.SemaphoreSlim das neue Abbruchframework in ihren Wait-Methoden. Sie können CancellationToken an die Methode übergeben. Wenn der Abbruch angefordert wird, wird das Ereignis aktiviert und löst eine OperationCanceledException aus.

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

Ein ausführlicheres Beispiel finden Sie unter Gewusst wie: 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. ein internes Abbruchtoken zusätzlich zu einem Token überwachen, das extern als Argument an einen Methodenparameter übergeben wurde. Erstellen Sie hierzu eine verknüpfte Tokenquelle, die zwei oder mehr Token in einem Token verknüpfen kann, wie im folgenden Beispiel dargestellt.

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
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();
            }
        }
    }
}

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 Gewusst wie: Lauschen auf mehrere Abbruchanforderungen.

Zusammenarbeit zwischen Bibliothekscode und Benutzercode

Durch das einheitliche Abbruchframework kann Bibliothekscode Benutzercode abbrechen, und Benutzercode kann Bibliothekscode auf eine kooperative Weise abbrechen. Die problemlose Zusammenarbeit ist auf beiden Seiten von der Einhaltung der folgenden Richtlinien abhängig:

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

  • Wenn Bibliothekscode Benutzercode aufruft, sollte der Bibliothekscode eine OperationCanceledException(externalToken) als kooperativen Abbruch interpretieren und nicht notwendigerweise als Fehlerausnahme.

  • 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 unter Gewusst wie: Abbrechen einer PLINQ-Abfrage.

Siehe auch

Weitere Ressourcen

Grundlagen des verwalteten Threadings