Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Ab .NET Framework 4 verwendet .NET ein einheitliches Modell für den kooperativen Abbruch asynchroner oder lang ausgeführter synchroner Vorgänge. Dieses Modell basiert auf einem einfachen Objekt, das als Abbruchtoken bezeichnet wird. Das Objekt, das einen oder mehrere abbruchbare 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 weitergeben. Zu einem späteren Zeitpunkt kann das Objekt, das das Token erstellt hat, es verwenden, um anzufordern, dass die Vorgänge die Ausführung beenden. Nur das anfordernde Objekt kann die Abbruchanforderung ausstellen, und jeder Listener ist dafür verantwortlich, die Anforderung zu notieren 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 jeden Thread bereit, um auf den Abbruch zu reagieren.
Rufen Sie die CancellationTokenSource.Cancel-Methode auf, um eine Benachrichtigung über den Abbruch bereitzustellen.
Von Bedeutung
Der CancellationTokenSource-Klasse implementiert die IDisposable-Schnittstelle. Sie sollten unbedingt die CancellationTokenSource.Dispose-Methode aufrufen, nachdem Sie die Abbruchtoken-Quelle verwendet haben, um alle darin enthaltenen nicht verwalteten Ressourcen freizugeben.
Die folgende Abbildung zeigt die Beziehung zwischen einer Tokenquelle und allen Kopien des Tokens.
Das kooperative Abbruchmodell erleichtert das Erstellen von abbruchfähigen Anwendungen und Bibliotheken und unterstützt die folgenden Features:
Die Stornierung erfolgt kooperativ und wird dem Zuhörer nicht aufgezwungen. Der Listener bestimmt, wie die ordnungsgemäße Beendigung als Reaktion auf eine Abbruchanforderung durchgeführt wird.
Das Anfordern unterscheidet sich vom Zuhören. Ein Objekt, das einen abbruchbaren Vorgang aufruft, kann steuern, wann (oder ob überhaupt) ein Abbrechen angefordert wird.
Das anfordernde Objekt gibt die Abbruchanforderung mit nur einem Methodenaufruf an alle Kopien des Tokens aus.
Ein Listener kann mehrere Token gleichzeitig abhören, indem er sie mit einem verknüpften Token verknüpft.
Benutzercode kann Abbruchanforderungen von Bibliothekscode bemerken und beantworten, und Bibliothekscode kann Abbruchanforderungen von Benutzercode bemerken und beantworten.
Listener können durch Abruf, Rückrufregistrierung oder Warten auf Wait-Handles über Abbruchanforderungen benachrichtigt werden.
Abbruchtypen
Das Abbruchframework wird als eine Reihe verwandter Typen implementiert, die in der folgenden Tabelle aufgeführt sind.
Typname | BESCHREIBUNG |
---|---|
CancellationTokenSource | Objekt, das ein Abbruchtoken erstellt und auch die Abbruchanforderung für alle Kopien dieses Tokens ausgibt. |
CancellationToken | Einfacher Werttyp, der an einen oder mehrere Listener übergeben wird, in der Regel als Methodenparameter. 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 Personen zu benachrichtigen, dass sie auf eine Abbruchanforderung geantwortet haben. |
Das Abbruchmodell ist in .NET in mehreren Typen integriert. Die wichtigsten sind System.Threading.Tasks.Parallel, , System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> und System.Linq.ParallelEnumerable. Es wird empfohlen, dieses kooperative Abbruchmodell für alle neuen Bibliotheks- und Anwendungscodes zu verwenden.
Codebeispiel
Im folgenden Beispiel erstellt das anfordernde Objekt ein CancellationTokenSource Objekt und übergibt dann seine Token Eigenschaft an den abbruchbaren 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 älteren 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 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...
Vorgangsabbruch im Vergleich zum Objektabbruch
Im kooperativen Abbruchframework bezieht sich der Abbruch auf Vorgänge, nicht auf Objekte. Die Abbruchanforderung bedeutet, dass der Vorgang so bald wie möglich beendet werden soll, nachdem eine erforderliche Bereinigung durchgeführt wurde. Ein Abbruchtoken sollte auf einen "abbruchfähigen Vorgang" verweisen, dieser Vorgang kann jedoch 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 auf dem Vorgangsabbruchmechanismus basieren, indem Sie die CancellationToken.Register Methode aufrufen, 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 {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
Wenn ein Objekt mehrere gleichzeitige abbrechbare Vorgänge unterstützt, übergeben Sie ein separates Token als Eingabe an jeden eindeutigen abbruchfähigen Vorgang. Auf diese Weise kann ein Vorgang abgebrochen werden, ohne dass sich dies auf die anderen auswirkt.
Zuhören und Reagieren auf Abbruchanforderungen
Im Benutzerdelegat bestimmt der Implementierer eines abbruchfähigen Vorgangs, wie der Vorgang als Reaktion auf eine Abbruchanforderung beendet wird. In vielen Fällen kann der Benutzerdelegat einfach jede erforderliche Bereinigung durchführen und dann sofort zurückgeben.
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 beendet der Delegat den Vorgang korrekt, indem er die ThrowIfCancellationRequested-Methode aufruft, was dazu führt, dass ein OperationCanceledException ausgelöst wird. 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 "Abbruch der Aufgabe".
Lauschen durch Abruf
Bei lang andauernden Berechnungen, die schleifen oder rekursieren, können Sie auf eine Abbruchanforderung lauschen, indem Sie den Wert der CancellationToken.IsCancellationRequested Eigenschaft regelmäßig abfragen. Wenn der Wert lautet true
, sollte die Methode so schnell wie möglich bereinigt und beendet werden. Die optimale Häufigkeit der Abstimmung hängt von der Art der Anwendung ab. Es liegt an dem Entwickler, die beste Abrufhäufigkeit für jedes bestimmte Programm zu bestimmen. 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 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
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, mit der die Blockierung der Methode aufgehoben wird, 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 verwenden, um eine asynchrone Webanforderung abzubrechen.
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
Das CancellationTokenRegistration Objekt verwaltet die Threadsynchronisierung und stellt sicher, dass der Rückruf zu einem genauen Zeitpunkt nicht mehr ausgeführt wird.
Um die Reaktionsfähigkeit des Systems zu gewährleisten und Deadlocks zu vermeiden, müssen beim Registrieren von Rückrufen die folgenden Richtlinien befolgt 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. Nach der Rückkehr von
Dispose
können Sie alle Ressourcen freigeben, die vom Callback benötigt werden.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 mit dem System.Threading.CancellationTokenRegistration den Konstruktor, der es Ihnen ermöglicht anzugeben, dass der Ziel-Synchronisationskontext der aktive SynchronizationContext.Current ist. Das Ausführen von manuellem Threading in einem Rückruf kann zu einer Blockierung führen.
Ein vollständigeres Beispiel finden Sie unter Registrieren von Rückrufen für Stornierungsanforderungen.
Lauschen mithilfe eines Wait-Handles
Wenn ein abbruchbarer Vorgang blockieren kann, während er auf einen Synchronisationsgrundtyp wie ein System.Threading.ManualResetEvent oder System.Threading.Semaphore wartet, können Sie die Eigenschaft CancellationToken.WaitHandle verwenden, damit der Vorgang sowohl auf das Ereignis als auch auf 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 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))
System.Threading.ManualResetEventSlim und System.Threading.SemaphoreSlim unterstützen das Stornierungs-Framework 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. Beispielsweise muss ein abbrechbarer Vorgang zusätzlich zu einem Token, das extern als Argument an einen Methodenparameter übergeben wird, ein internes Abbruchtoken überwachen. Erstellen Sie dazu eine verknüpfte Tokenquelle, die zwei oder mehr Token mit einem Token verknüpfen 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 die verknüpfte Tokenquelle Dispose
aufrufen müssen, wenn Sie damit fertig sind. Für ein umfassenderes Beispiel sehen Sie Wie man auf mehrere Stornierungsanfragen hört.
Zusammenarbeit 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. Eine reibungslose Zusammenarbeit hängt von jeder Seite ab, die folgende Richtlinien befolgt:
Wenn Bibliothekscode abbruchfähige Vorgänge bereitstellt, sollte er auch öffentliche Methoden bereitstellen, 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 diesen Richtlinien entsprechen. Weitere Informationen finden Sie unter Aufgabenabbruch und Vorgehensweise: Abbrechen einer PLINQ-Abfrage.
Siehe auch
- Managed Threading Basics (Grundlagen des verwalteten Threadings)