Condividi tramite


Annullamento nei thread gestiti

A partire da .NET Framework 4, .NET usa un modello unificato per l'annullamento cooperativo di operazioni sincrone asincrone o a esecuzione prolungata. Questo modello si basa su un oggetto leggero denominato token di annullamento. L'oggetto che richiama una o più operazioni annullabili, ad esempio creando nuovi thread o attività, passa il token a ogni operazione. Le singole operazioni possono a loro volta passare copie del token ad altre operazioni. In un secondo momento, l'oggetto che ha creato il token può usarlo per richiedere che le operazioni interrompano le operazioni eseguite. Solo l'oggetto richiedente può emettere la richiesta di annullamento e ogni listener è responsabile della notazione della richiesta e della relativa risposta in modo appropriato e tempestivo.

Il modello generale per l'implementazione del modello di annullamento cooperativo è:

  • Creare un'istanza di un oggetto CancellationTokenSource, che gestisce e invia notifiche di annullamento ai singoli token di annullamento.

  • Passa il token restituito dalla proprietà CancellationTokenSource.Token a ogni attività o thread che ascolta per la cancellazione.

  • Fornire un meccanismo per ogni attività o thread per rispondere all'annullamento.

  • Chiamare il metodo CancellationTokenSource.Cancel per fornire la notifica dell'annullamento.

Importante

La classe CancellationTokenSource implementa l'interfaccia IDisposable. Dovresti assicurarti di chiamare il metodo CancellationTokenSource.Dispose quando hai finito di utilizzare l'origine del token di annullamento per liberare qualsiasi risorsa non gestita che contiene.

La figura seguente illustra la relazione tra una sorgente di token e tutte le copie dei suoi token.

CancellationTokenSource e i token di annullamento

Il modello di annullamento cooperativo semplifica la creazione di applicazioni e librerie in grado di supportare le funzionalità seguenti:

  • L'annullamento è cooperativo e non viene forzato sul listener. Il listener determina come terminare in modo appropriato in risposta a una richiesta di annullamento.

  • La richiesta è diversa dall'ascolto. Un oggetto che richiama un'operazione annullabile può controllare quando viene richiesto (se mai) l'annullamento.

  • L'oggetto richiedente invia la richiesta di annullamento a tutte le copie del token usando una sola chiamata al metodo.

  • Un listener può ascoltare più token simultaneamente unendoli in un unico token collegato .

  • Il codice utente può notare e rispondere alle richieste di annullamento dal codice della libreria e il codice della libreria può notare e rispondere alle richieste di annullamento dal codice utente.

  • I listener possono ricevere una notifica delle richieste di annullamento eseguendo il polling, la registrazione del callback o attendendo gli handle di attesa.

Tipi di annullamento

Il framework di annullamento viene implementato come set di tipi correlati, elencati nella tabella seguente.

Nome del tipo Descrizione
CancellationTokenSource Oggetto che crea un token di annullamento e invia anche la richiesta di annullamento per tutte le copie di tale token.
CancellationToken Tipo di valore leggero passato a uno o più listener, in genere come parametro del metodo. I listener monitorano il valore della proprietà IsCancellationRequested del token eseguendo il polling, il callback o l'handle di attesa.
OperationCanceledException I sovraccarichi del costruttore di questa eccezione accettano un CancellationToken come parametro. I listener possono eventualmente generare questa eccezione per verificare l'origine dell'annullamento e notificare ad altri utenti che ha risposto a una richiesta di annullamento.

Il modello di annullamento è integrato in .NET in diversi tipi. I più importanti sono System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult> e System.Linq.ParallelEnumerable. È consigliabile usare questo modello di annullamento cooperativo per tutte le nuove librerie e il codice dell'applicazione.

Esempio di codice

Nell'esempio seguente l'oggetto richiedente crea un oggetto CancellationTokenSource e quindi passa la relativa proprietà Token all'operazione annullabile. L'operazione che riceve la richiesta monitora il valore della proprietà IsCancellationRequested del token eseguendo il polling. Quando il valore diventa true, il listener può terminare in qualsiasi modo appropriato. In questo esempio, il metodo si limita a terminare, che è tutto ciò che è richiesto in molti casi.

Nota

Nell'esempio viene usato il metodo QueueUserWorkItem per dimostrare che il framework di annullamento cooperativo è compatibile con le API legacy. Per un esempio che usa il tipo di System.Threading.Tasks.Task preferito, vedere Procedura: Annullare un'attività e i relativi elementi figlio.

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

Annullamento dell'operazione rispetto all'annullamento dell'oggetto

Nel framework di annullamento cooperativo, l'annullamento fa riferimento a operazioni, non a oggetti. La richiesta di annullamento indica che l'operazione deve essere interrotta il prima possibile dopo l'esecuzione di tutte le operazioni di pulizia necessarie. Un token di annullamento deve fare riferimento a una "operazione annullabile", tuttavia tale operazione può essere implementata nel programma. Dopo che la proprietà IsCancellationRequested del token è stata impostata su true, non può essere reimpostata su false. Di conseguenza, i token di annullamento non possono essere riutilizzati dopo che sono stati annullati.

Se è necessario un meccanismo di annullamento degli oggetti, è possibile basarlo sul meccanismo di annullamento dell'operazione chiamando il metodo CancellationToken.Register, come illustrato nell'esempio seguente.

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

Se un oggetto supporta più operazioni annullabili simultanee, passare un token separato come input a ogni operazione annullabile distinta. In questo modo, un'operazione può essere annullata senza influire sugli altri.

Ascolto e risposta alle richieste di annullamento

Nel delegato utente, l'implementatore di un'operazione annullabile determina come terminare l'operazione in risposta a una richiesta di annullamento. In molti casi, il delegato dell'utente può eseguire solo la pulizia necessaria e quindi restituire immediatamente.

Tuttavia, in casi più complessi, potrebbe essere necessario che il delegato dell'utente comunichi al codice della libreria che si è verificato l'annullamento. In questi casi, il modo corretto per terminare l'operazione è che il delegato chiami il metodo ThrowIfCancellationRequested, che farà generare un OperationCanceledException. Il codice della libreria può intercettare questa eccezione nel thread delegato utente ed esaminare il token dell'eccezione per determinare se l'eccezione indica l'annullamento cooperativo o un'altra situazione eccezionale.

La classe Task gestisce OperationCanceledException in questo modo. Per altre informazioni, vedere Annullamento attività .

Ascolto tramite polling

Per i calcoli a esecuzione prolungata che eseguono cicli o ricorsioni, è possibile monitorare una richiesta di annullamento interrogando periodicamente il valore della proprietà CancellationToken.IsCancellationRequested. Se il valore è trueil metodo deve ripulire e terminare il più rapidamente possibile. La frequenza ottimale del polling dipende dal tipo di applicazione. Spetta allo sviluppatore determinare la frequenza di polling migliore per qualsiasi programma specifico. Il polling non influisce in modo significativo sulle prestazioni. Nell'esempio seguente viene illustrato un modo possibile per eseguire il polling.

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

Per un esempio più completo, vedere Procedura: Ascoltare le richieste di annullamento eseguendo il polling.

Ascolto registrando un callback

Alcune operazioni possono essere bloccate in modo che non possano controllare il valore del token di annullamento in modo tempestivo. Per questi casi, è possibile registrare un metodo di callback che sblocca il metodo quando viene ricevuta una richiesta di annullamento.

Il metodo Register restituisce un oggetto CancellationTokenRegistration utilizzato in modo specifico per questo scopo. Nell'esempio seguente viene illustrato come usare il metodo Register per annullare una richiesta Web asincrona.

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

L'oggetto CancellationTokenRegistration gestisce la sincronizzazione dei thread e garantisce che il callback arresti l'esecuzione in un momento preciso.

Per garantire la velocità di risposta del sistema e per evitare deadlock, è necessario seguire le linee guida seguenti durante la registrazione dei callback:

  • Il metodo di callback deve essere veloce perché viene chiamato in modo sincrono e pertanto la chiamata a Cancel non restituisce finché il callback non viene restituito.

  • Se si chiama Dispose mentre il callback è in esecuzione e si mantiene un blocco in attesa del callback, il programma può bloccarsi. Dopo il ritorno di Dispose, puoi liberare le risorse utilizzate dal callback.

  • I callback non devono eseguire alcun thread manuale o utilizzo manuale di SynchronizationContext in un callback. Se un callback deve essere eseguito in un thread specifico, usare il costruttore System.Threading.CancellationTokenRegistration che consente di specificare che syncContext di destinazione è il SynchronizationContext.Currentattivo. L'esecuzione del threading manuale in un callback può causare deadlock.

Per un esempio più completo, vedere Procedura: Registrare i callback per le richieste di annullamento.

Utilizzo di un indicatore di attesa per l'ascolto

Quando un'operazione annullabile può bloccarsi mentre attende una primitiva di sincronizzazione, ad esempio un System.Threading.ManualResetEvent o System.Threading.Semaphore, è possibile utilizzare la proprietà CancellationToken.WaitHandle per consentire all'operazione di attendere sia l'evento che la richiesta di annullamento. L'handle di attesa associato al token di annullamento verrà segnalato in risposta a una richiesta di annullamento, e il metodo può utilizzare il valore restituito del metodo WaitAny per determinare se è stato il token di annullamento a segnalare. L'operazione può quindi semplicemente uscire o generare un OperationCanceledException, in base alle esigenze.

// 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 e System.Threading.SemaphoreSlim supportano entrambi il framework di annullamento nei loro metodi di Wait. È possibile passare il CancellationToken al metodo e, quando viene richiesto l'annullamento, l'evento si riattiva e genera un 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)

Per un esempio più completo, vedere Come ascoltare le richieste di annullamento con handle di attesa.

Ascolto simultaneo di diversi token

In alcuni casi, un listener potrebbe dover restare in ascolto di più token di annullamento contemporaneamente. Ad esempio, un'operazione annullabile può dover monitorare un token di annullamento interno oltre a un token passato esternamente come argomento a un parametro del metodo. A tale scopo, creare un'origine token collegata in grado di unire due o più token in un token, come illustrato nell'esempio seguente.

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

Si noti che è necessario chiamare Dispose sull'origine del token collegato al termine dell'operazione. Per un esempio più completo, vedere Procedura: Ascoltare più richieste di annullamento.

Cooperazione tra codice di libreria e codice utente

Il framework di annullamento unificato consente al codice della libreria di annullare il codice utente e al codice utente di annullare il codice della libreria in modo cooperativo. La cooperazione efficace dipende da ciascuna parte che segua queste linee guida.

  • Se il codice della libreria fornisce operazioni annullabili, deve anche fornire metodi pubblici che accettano un token di annullamento esterno in modo che il codice utente possa richiedere l'annullamento.

  • Se il codice della libreria chiama il codice utente, il codice della libreria deve interpretare OperationCanceledException(externalToken) come annullamento cooperativoe non necessariamente come eccezione di fallimento.

  • I delegati utente devono tentare di rispondere alle richieste di annullamento dal codice di libreria in modo tempestivo.

System.Threading.Tasks.Task e System.Linq.ParallelEnumerable sono esempi di classi che seguono queste linee guida. Per ulteriori informazioni, vedere Annullamento delle attività e Come: annullare una query PLINQ.

Vedere anche