Annulering in beheerde threads
Vanaf .NET Framework 4 maakt .NET gebruik van een uniform model voor het opzeggen van asynchrone of langdurige synchrone bewerkingen. Dit model is gebaseerd op een lichtgewicht object dat een annuleringstoken wordt genoemd. Het object dat een of meer geannuleerde bewerkingen aanroept, bijvoorbeeld door nieuwe threads of taken te maken, geeft het token door aan elke bewerking. Afzonderlijke bewerkingen kunnen op hun beurt kopieën van het token doorgeven aan andere bewerkingen. Op een later tijdstip kan het object dat het token heeft gemaakt, het gebruiken om aan te vragen of de bewerkingen stoppen wat ze doen. Alleen het aanvragende object kan de annuleringsaanvraag uitgeven en elke listener is verantwoordelijk voor het op de juiste en tijdige wijze noteren van de aanvraag en erop te reageren.
Het algemene patroon voor het implementeren van het samenwerkingsannuleringsmodel is:
Instantieer een CancellationTokenSource-object, dat annuleringsmeldingen beheert en verzendt naar de afzonderlijke annuleringstokens.
Geef het token door dat door de CancellationTokenSource.Token eigenschap wordt geretourneerd aan elke taak of thread die luistert voor annulering.
Geef een mechanisme op voor elke taak of thread om te reageren op annulering.
Roep de CancellationTokenSource.Cancel methode aan om een melding van annulering op te geven.
Belangrijk
Met de klasse CancellationTokenSource wordt de IDisposable-interface geïmplementeerd. Zorg ervoor dat u de methode CancellationTokenSource.Dispose aanroept wanneer u klaar bent met het gebruik van de annuleringstokenbron om onbeheerde resources vrij te maken die deze bevat.
In de volgende afbeelding ziet u de relatie tussen een tokenbron en alle kopieën van het token.
Met het samenwerkingsannuleringsmodel kunt u eenvoudiger annuleringsbewuste toepassingen en bibliotheken maken en ondersteunt het de volgende functies:
Annulering is coöperatief en wordt niet aan de luisteraar opgedrongen. De luisteraar bepaalt hoe een annuleringsverzoek op een rustige manier wordt afgehandeld.
Verzoeken is iets anders dan luisteren. Een object dat een annuleringsbewerking aanroept, kan bepalen wanneer (indien ooit) annulering wordt aangevraagd.
Het aanvragende object geeft de annuleringsaanvraag uit aan alle kopieën van het token met behulp van slechts één methode-aanroep.
Een listener kan tegelijkertijd naar meerdere tokens luisteren door ze te koppelen aan één gekoppeld token.
Gebruikerscode kan annuleringsaanvragen van bibliotheekcode opmerken en erop reageren, en bibliotheekcode kan annuleringsaanvragen van gebruikerscode opmerken en erop reageren.
Listeners kunnen op de hoogte worden gesteld van annuleringsaanvragen door polling, callback-registratie of wachten op wachtgrepen.
Annuleringstypen
Het annuleringsframework wordt geïmplementeerd als een set gerelateerde typen, die worden vermeld in de volgende tabel.
Typenaam | Beschrijving |
---|---|
CancellationTokenSource | Object dat een annuleringstoken maakt en ook de annuleringsaanvraag voor alle kopieën van dat token uitgeeft. |
CancellationToken | Lichtgewicht waardetype dat wordt doorgegeven aan een of meer listeners, meestal als methodeparameter. Listeners controleren de waarde van de IsCancellationRequested -eigenschap van het token via polling, callback of een wacht-handle. |
OperationCanceledException | Overbelastingen van de constructor van deze uitzondering accepteren een CancellationToken als parameter. Listeners kunnen deze uitzondering eventueel genereren om de bron van de annulering te controleren en anderen te informeren dat deze heeft gereageerd op een annuleringsaanvraag. |
Het annuleringsmodel is in verschillende typen geïntegreerd in .NET. De belangrijkste zijn System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult> en System.Linq.ParallelEnumerable. U wordt aangeraden dit coöperatieve annuleringsmodel te gebruiken voor alle nieuwe bibliotheek- en toepassingscode.
Codevoorbeeld
In het volgende voorbeeld maakt het aanvraagobject een CancellationTokenSource-object aan en wordt de eigenschap Token vervolgens doorgegeven aan de annuleringsbewerking. De bewerking die de aanvraag ontvangt, bewaakt de waarde van de eigenschap IsCancellationRequested van het token door polling uit te voeren. Wanneer de waarde true
wordt, kan de luisterfunctie op een passende manier beëindigd worden. In dit voorbeeld stopt de methode gewoon, wat in veel gevallen alles is wat nodig is.
Notitie
In het voorbeeld wordt de QueueUserWorkItem methode gebruikt om aan te tonen dat het samenwerkingsannuleringsframework compatibel is met verouderde API's. Zie Hoe: Annuleer een taak en de bijbehorende takenvoor een voorbeeld van het gebruik van het voorkeurstype System.Threading.Tasks.Task.
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...
Annulering van operatie versus objectannulering
In het coöperatief annuleringskader verwijst annulering naar bewerkingen, niet naar objecten. De annuleringsaanvraag betekent dat de bewerking zo snel mogelijk moet stoppen nadat een vereiste opschoonbewerking is uitgevoerd. Eén annuleringstoken moet verwijzen naar één 'annuleerbare bewerking', maar die bewerking kan worden geïmplementeerd in uw programma. Nadat de eigenschap IsCancellationRequested van het token is ingesteld op true
, kan deze niet opnieuw worden ingesteld op false
. Daarom kunnen annuleringstokens niet opnieuw worden gebruikt nadat ze zijn geannuleerd.
Als u een mechanisme voor objectannulering nodig hebt, kunt u dit baseren op het annuleringsmechanisme van de bewerking door de methode CancellationToken.Register aan te roepen, zoals wordt weergegeven in het volgende voorbeeld.
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
Als een object meer dan één gelijktijdige annuleerbare bewerking ondersteunt, geeft u een afzonderlijk token door als invoer voor elke afzonderlijke annuleringsbewerking. Op die manier kan één bewerking worden geannuleerd zonder dat dit van invloed is op de andere bewerkingen.
Luisteren en reageren op annuleringsaanvragen
In de gebruikersdelegaat bepaalt de implementator van een annuleerbare bewerking hoe de bewerking moet worden beëindigd als reactie op een annuleringsaanvraag. In veel gevallen kan de gemachtigde van de gebruiker gewoon elke vereiste opschoning uitvoeren en vervolgens onmiddellijk terugkeren.
In complexere gevallen kan het echter nodig zijn dat de gemachtigde van de gebruiker bibliotheekcode op de hoogte stelt van de annulering. In dergelijke gevallen is de juiste manier om de bewerking te beëindigen, dat de gedelegeerde de ThrowIfCancellationRequestedaanroept, waardoor een OperationCanceledException wordt opgeworpen. Bibliotheekcode kan deze uitzondering op de gedelegeerde thread van de gebruiker opvangen en het token van de uitzondering onderzoeken om te bepalen of de uitzondering wijst op coöperatieve annulering of een andere uitzonderlijke situatie.
De Task klasse verwerkt OperationCanceledException op deze manier. Zie Taakannuleringvoor meer informatie.
Luisteren door middel van polling
Voor langlopende berekeningen die een lus hebben of herhalingen bevatten, kunt u een annuleringsaanvraag controleren door periodiek de waarde van de eigenschap CancellationToken.IsCancellationRequested te controleren. Als de waarde true
is, moet de methode zo snel mogelijk worden opgeschoond en beëindigd. De optimale frequentie van polling is afhankelijk van het type toepassing. Het is aan de ontwikkelaar om de beste pollingfrequentie voor een bepaald programma te bepalen. Polling zelf heeft geen aanzienlijke invloed op de prestaties. In het volgende voorbeeld ziet u een mogelijke manier om te peilen.
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
Zie voor een vollediger voorbeeld Hoe te: Luisteren naar annuleringsverzoeken door te peilen.
Luisteren door een callback te registreren
Sommige bewerkingen kunnen zodanig worden geblokkeerd dat ze de waarde van het annuleringstoken niet tijdig kunnen controleren. Voor deze gevallen kunt u een callback-methode registreren waarmee de methode wordt gedeblokkeerd wanneer een annuleringsaanvraag wordt ontvangen.
De methode Register retourneert een CancellationTokenRegistration-object dat specifiek voor dit doel wordt gebruikt. In het volgende voorbeeld ziet u hoe u de methode Register gebruikt om een asynchrone webaanvraag te annuleren.
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
Het CancellationTokenRegistration-object beheert threadsynchronisatie en zorgt ervoor dat de callback op een nauwkeurig tijdstip stopt met uitvoeren.
Om de reactiesnelheid van het systeem te waarborgen en impasses te voorkomen, moeten de volgende richtlijnen worden gevolgd bij het registreren van callbacks:
De callback-methode moet snel zijn omdat deze synchroon wordt aangeroepen en daarom wordt de aanroep naar Cancel niet geretourneerd totdat de callback wordt geretourneerd.
Als u Dispose aanroept terwijl de callback wordt uitgevoerd en u een vergrendeling vasthoudt waarop de callback wacht, kan uw programma vastlopen. Nadat
Dispose
retourneert, kunt u alle resources die nodig zijn voor de callback gratis maken.Callbacks moeten geen handmatig gebruik van threads of SynchronizationContext uitvoeren in een callback. Als een callback moet worden uitgevoerd op een bepaalde thread, gebruikt u de System.Threading.CancellationTokenRegistration constructor waarmee u kunt opgeven dat de doelsyncContext de actieve SynchronizationContext.Currentis. Handmatige threading uitvoeren in een callback kan een deadlock veroorzaken.
Zie Procedure: Callbacks registreren voor annuleringsaanvragenvoor een volledig voorbeeld.
Luisteren met behulp van een wachtgreep
Wanneer een geannuleerde bewerking kan worden geblokkeerd terwijl deze wacht op een synchronisatieprimitief zoals een System.Threading.ManualResetEvent of System.Threading.Semaphore, kunt u de eigenschap CancellationToken.WaitHandle gebruiken om de bewerking in te schakelen om te wachten op zowel de gebeurtenis als de annuleringsaanvraag. De wachtgreep van het annuleringstoken wordt gesignaleerd als reactie op een annuleringsaanvraag en de methode kan de retourwaarde van de methode WaitAny gebruiken om te bepalen of het het annuleringstoken was dat is gesignaleerd. De bewerking kan vervolgens gewoon worden afgesloten of een OperationCanceledExceptiongooien, als dat gepast is.
// 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 en System.Threading.SemaphoreSlim steunen beide het annuleringsframework in hun Wait
methoden. U kunt de CancellationToken doorgeven aan de methode en wanneer de annulering wordt aangevraagd, wordt de gebeurtenis wakker en wordt er een OperationCanceledExceptiongegenereerd.
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)
Voor een vollediger voorbeeld, zie Hoe te luisteren naar annuleringsaanvragen die wachtgrepen hebben.
Gelijktijdig luisteren naar meerdere tokens
In sommige gevallen moet een listener mogelijk tegelijkertijd naar meerdere annuleringstokens luisteren. Een annuleerbare bewerking moet bijvoorbeeld een intern annuleringstoken bewaken naast een token dat extern is doorgegeven als argument voor een methodeparameter. Hiervoor maakt u een gekoppelde tokenbron die twee of meer tokens aan één token kan koppelen, zoals wordt weergegeven in het volgende voorbeeld.
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
U moet Dispose
aanroepen op de gekoppelde tokenbron wanneer u er klaar mee bent. Zie Procedure: Luisteren naar meerdere annuleringsaanvragenvoor een volledig voorbeeld.
Samenwerking tussen bibliotheekcode en gebruikerscode
Het geïntegreerde annuleringsframework maakt het mogelijk dat bibliotheekcode gebruikerscode kan annuleren en dat gebruikerscode op een coöperatieve manier bibliotheekcode kan annuleren. Een soepele samenwerking hangt af van elk van de volgende richtlijnen:
Als bibliotheekcode annuleringsbewerkingen biedt, moet deze ook openbare methoden bieden die een extern annuleringstoken accepteren, zodat gebruikerscode annulering kan aanvragen.
Als bibliotheekcode gebruikerscode aanroept, moet de bibliotheekcode een OperationCanceledException(externalToken) interpreteren als coöperatieve annuleringen niet noodzakelijkerwijs als een foutuitzondering.
Gebruikersvertegenwoordigers moeten proberen tijdig te reageren op annuleringsaanvragen van de bibliotheekcode.
System.Threading.Tasks.Task en System.Linq.ParallelEnumerable zijn voorbeelden van klassen die aan deze richtlijnen voldoen. Zie Taakannulering en Procedure: Een PLINQ-query annulerenvoor meer informatie.