Annullamento in thread gestiti

A partire da .NET Framework 4, .NET usa un modello unificato per l'annullamento cooperativo di operazioni asincrone o di operazioni sincrone a esecuzione prolungata. Questo modello è basato su un oggetto leggero chiamato token di annullamento. L'oggetto che richiama una o più operazioni annullabili, ad esempio tramite la creazione di nuovi thread o attività, passa il token a ogni operazione. 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 arrestino le rispettive attività. Solo l'oggetto richiedente può inviare la richiesta di annullamento e ogni listener è responsabile del rilevamento della richiesta e della relativa risposta in modo appropriato e tempestivo.

Il criterio generale per implementare il modello di annullamento cooperativo è il seguente:

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

  • Passare il token restituito dalla proprietà CancellationTokenSource.Token a ogni attività o thread in attesa di annullamento.

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

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

Importante

La classe CancellationTokenSource implementa l'interfaccia IDisposable. Assicurarsi di chiamare il metodo CancellationTokenSource.Dispose dopo aver usato l'origine del token di annullamento per liberare qualsiasi risorsa gestita che contiene.

L'immagine seguente mostra la relazione tra l'origine di un token e tutte le copie del token.

CancellationTokenSource and cancellation tokens

Il modello di annullamento cooperativo semplifica la creazione di applicazioni e librerie in grado di riconoscere l'annullamento e supporta le funzionalità seguenti:

  • L'annullamento è cooperativo e non viene forzato nel listener. Il listener determina come eseguire normalmente la terminazione in risposta a una richiesta di annullamento.

  • La richiesta è diversa dall'ascolto. Un oggetto che richiama un'operazione annullabile può controllare il momento (se applicabile) in cui è necessario un annullamento.

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

  • Un listener può restare in ascolto di più token simultaneamente unendoli in un token collegato.

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

  • I listener possono ricevere notifiche delle richieste di annullamento tramite polling, registrazione dei callback o attesa di handle di attesa.

Tipi di annullamento

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

Nome tipo Descrizione
CancellationTokenSource Oggetto che crea un token di annullamento e che invia inoltre la richiesta di annullamento per tutte le copie del token.
CancellationToken Tipo di valore leggero passato a uno o più listener, in genere come parametro di un metodo. I listener monitorano il valore della proprietà IsCancellationRequested del token tramite polling, callback o handle di attesa.
OperationCanceledException Gli overload del costruttore di questa eccezione accettano un oggetto CancellationToken come parametro. I listener possono facoltativamente generare questa eccezione per verificare l'origine dell'annullamento e notificare ad altri 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 tutto il nuovo codice di libreria e applicazione.

Esempio di codice

Nell'esempio seguente l'oggetto richiedente crea un oggetto CancellationTokenSource e quindi ne passa la proprietà Token all'operazione annullabile. L'operazione che riceve la richiesta monitora il valore della proprietà IsCancellationRequested del token tramite polling. Quando il valore diventa true, il listener può essere terminato nel modo più appropriato. In questo esempio avviene semplicemente l'uscita del metodo, che nella maggior parte dei casi è tutto ciò che serve.

Nota

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

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

Confronto tra l'annullamento di operazioni e l'annullamento di oggetti

Nel framework di annullamento cooperativo, l'annullamento è riferito alle operazioni e non agli oggetti. La richiesta di annullamento significa che l'operazione deve essere arrestata il prima possibile dopo l'esecuzione della pulizia necessaria. Un token di annullamento deve fare riferimento a una "operazione annullabile", ma tale operazione può essere implementata nel programma. Se la proprietà IsCancellationRequested del token viene impostata su true, non può più essere reimpostata su false. Di conseguenza, i token di annullamento non possono essere riutilizzati una volta annullati.

Se è necessario un meccanismo di annullamento di oggetti, è possibile basarlo sul meccanismo di annullamento di operazioni chiamando il metodo CancellationToken.Register, come mostrato 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 {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

Se un oggetto supporta più operazioni annullabili simultanee, passare un token separato come input a ogni operazione annullabile diversa. In questo modo, è possibile annullare un'operazione senza influire sulle altre.

Ascolto e risposta alle richieste di annullamento

Nel delegato dell'utente l'implementatore di un'operazione annullabile determina il modo in cui 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 riprendere immediatamente.

Tuttavia, in casi più complessi il delegato dell'utente potrebbe dover notificare al codice di libreria il verificarsi dell'annullamento. In questi casi, il modo corretto di terminare l'operazione per il delegato consiste nel chiamare il metodo ThrowIfCancellationRequested, che provoca la generazione di un'eccezione OperationCanceledException. Il codice di libreria può rilevare l'eccezione nel thread del delegato dell'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 Task Cancellation.

Ascolto tramite polling

Per calcoli a esecuzione prolungata che eseguono cicli o sono ricorsivi, è possibile restare in ascolto di una richiesta di annullamento eseguendo periodicamente il polling del valore della proprietà CancellationToken.IsCancellationRequested. Se il valore della proprietà è true, il metodo deve eseguire la pulizia e quindi deve essere terminato il più rapidamente possibile. La frequenza di polling ottimale dipende dal tipo di applicazione. È compito dello sviluppatore determinare la migliore frequenza di polling per qualsiasi programma specifico. Il polling in sé non ha un impatto significativo sulle prestazioni. L'esempio seguente mostra uno dei modi in cui è possibile 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 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

Per un esempio più completo, vedere Procedura: Mettersi in ascolto di richieste di annullamento tramite polling.

Ascolto tramite registrazione di un callback

Alcune operazioni possono venire bloccate in modo da non poter controllare il valore del token di annullamento in modo tempestivo. In 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 che viene usato per questo scopo specifico. L'esempio seguente mostra come usare il metodo Register per annullare una richiesta Web asincrona.

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

L'oggetto CancellationTokenRegistration gestisce la sincronizzazione dei thread e garantisce l'arresto dell'esecuzione del callback in un momento preciso.

Per garantire velocità di risposta del sistema ed evitare deadlock, è necessario seguire le linee guida indicate di seguito per la registrazione dei callback:

  • Il metodo di callback deve essere rapido perché viene chiamato in modo sincrono e di conseguenza la chiamata a Cancel non viene restituita fino alla restituzione del callback.

  • Se si chiama Dispose durante l'esecuzione del callback e si mantiene un blocco di cui il callback è in attesa, il programma può subire un deadlock. Quando viene restituito Dispose, è possibile liberare qualsiasi risorsa necessaria per il callback.

  • I callback non devono eseguire alcun thread manuale o utilizzo di SynchronizationContext in un callback. Se un callback deve essere eseguito in un determinato thread, usare il costruttore System.Threading.CancellationTokenRegistration, che permette di specificare che l'oggetto syncContext di destinazione è l'oggetto SynchronizationContext.Current attivo. L'esecuzione manuale di threading in un callback può provocare un deadlock.

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

Ascolto tramite un handle di attesa

Quando un'operazione annullabile può restare bloccata mentre è in attesa di una primitiva di sincronizzazione come System.Threading.ManualResetEvent o System.Threading.Semaphore, è possibile usare la proprietà CancellationToken.WaitHandle per permettere all'operazione di attendere sia l'evento sia la richiesta di annullamento. L'handle di attesa del token di annullamento verrà segnalato in risposta a una richiesta di annullamento e il metodo può usare il valore restituito del metodo WaitAny per determinare se la segnalazione è stata eseguita dal token di annullamento. L'operazione può quindi semplicemente uscire oppure generare un'eccezione OperationCanceledException, a seconda del comportamento più appropriato.

// 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 rispettivi metodi Wait. È possibile passare CancellationToken al metodo e quando viene richiesto l'annullamento, l'evento viene attivato e genera un'eccezione 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 Procedura: Mettersi in ascolto di richieste di annullamento con handle di attesa.

Ascolto di più token simultaneamente

In alcuni casi, un listener può dover essere in ascolto di più token di annullamento simultaneamente. Ad esempio, un'operazione di annullamento può dover monitorare un token di annullamento interno oltre a un token passato esternamente come argomento al parametro di un metodo. A questo scopo, creare l'origine di un token collegato in grado di unire due o più token in uno solo, come mostrato 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

Notare che è necessario chiamare Dispose nell'origine del token collegato al suo completamento. Per un esempio più completo, vedere Procedura: Ascolto di più richieste di annullamento.

Cooperazione tra codice di libreria e codice utente

Il framework di annullamento unificato permette al codice di libreria di annullare il codice utente e al codice utente di annullare il codice libreria in modo cooperativo. Una cooperazione uniforme dipende da ognuno dei due lati in base alle linee guida seguenti:

  • Se il codice di 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 di libreria esegue chiamate nel codice utente, deve interpretare un oggetto OperationCanceledException(externalToken) come annullamento cooperativo e non necessariamente come eccezione di errore.

  • I delegati dell'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 altre informazioni, vedere Annullamento delle attività e Procedura: Annullare una query PLINQ.

Vedi anche