Condividi tramite


Annullamento

In .NET Framework versione 4 viene introdotto un nuovo modello unificato per l'annullamento cooperativo di operazioni asincrone o sincrone a esecuzione prolungata. Questo modello è basato su un oggetto leggero denominato token di annullamento. L'oggetto che richiama un'operazione annullabile, ad esempio creando un nuovo thread o un'attività, passa il token all'operazione. Tale operazione può a sua volta passare copie del token ad altre operazioni. In un secondo momento, l'oggetto che ha creato il token può utilizzarlo per richiedere l'interruzione dell'attività di un'operazione. Solo l'oggetto richiedente può inviare la richiesta di annullamento e ogni listener è responsabile di rilevare la richiesta e rispondere in modo tempestivo. Nella figura seguente viene illustrata la relazione tra l'origine di un token e tutte le copie del token.

CancellationTokenSource e CancellationTokens

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

  • L'annullamento è cooperativo e non imposto nel listener. Il listener determina come terminare in modo automatico l'attività in risposta a una richiesta di annullamento.

  • La richiesta è distinta dall'ascolto. Un oggetto che richiama un'operazione annullabile può controllare quando viene richiesto l'annullamento, se ciò avviene.

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

  • Un listener può essere in ascolto di più token contemporaneamente unendoli in un token collegato.

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

  • I listener possono ricevere una notifica delle richieste di annullamento tramite polling, registrazione di callback o attesa negli handle di attesa.

Nuovi tipi di annullamento

Il nuovo 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 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 del metodo. I listener eseguono il monitoraggio del valore della proprietà IsCancellationRequested del token tramite polling, callback o handle di attesa.

OperationCanceledException

I nuovi overload di questa eccezione accettano un oggetto CancellationToken come parametro di input. I listener possono, facoltativamente, generare questa eccezione per verificare l'origine dell'annullamento e notificare ad altri componenti la risposta a una richiesta di annullamento.

Il nuovo modello di annullamento è integrato in .NET Framework 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 utilizzare questo nuovo modello di annullamento per tutto il nuovo codice di libreria e dell'applicazione.

Esempio di codice

Nell'esempio seguente l'oggetto richiedente crea un oggetto CancellationTokenSource, quindi passa la proprietà Token all'operazione annullabile. L'operazione che riceve la richiesta esegue il monitoraggio del valore della proprietà IsCancellationRequested del token tramite polling. Quando il valore diventa true, il listener può terminare nel modo appropriato. In questo esempio il metodo termina semplicemente, che è l'unica operazione richiesta, in molti casi.

NotaNota

In questo esempio viene utilizzato il metodo QueueUserWorkItem per illustrare che il nuovo framework di annullamento è compatibile con le API legacy.Per un esempio in cui viene utilizzato il nuovo tipo System.Threading.Tasks.Task, che è il tipo consigliato, vedere Procedura: annullare un'attività e i relativi figli.

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

Confronto tra annullamento di operazioni e annullamento di oggetti

Nel nuovo framework di annullamento, l'annullamento si riferisce alle operazioni e non agli oggetti. La richiesta di annullamento indica che l'operazione deve essere arrestata il prima possibile dopo l'esecuzione di eventuali operazioni di pulizia richieste. Un token di annullamento deve fare riferimento a un'"operazione annullabile", indipendentemente dal modo in cui tale operazione possa essere implementata nel programma. Dopo che la proprietà IsCancellationRequested del token è stata impostata su true, non è possibile ripristinare il valore false. I token di annullamento non possono pertanto essere riutilizzati dopo essere stati annullati.

Se è necessario un meccanismo di annullamento di oggetti, è possibile basarlo sul meccanismo di annullamento delle operazioni, come illustrato nell'esempio seguente.

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

Se un oggetto supporta più di un'operazione annullabile simultanea, passare un token separato come input per ogni operazione annullabile distinta. In questo modo, un'operazione può essere annullata senza influenzare le altre.

Ascolto e risposta alle richieste di annullamento

Il responsabile dell'implementazione di un'operazione annullabile determina, nel delegato dell'utente, come terminare l'operazione in risposta a una richiesta di annullamento. In molti casi, il delegato dell'utente potrà eseguire semplicemente la pulizia necessaria e terminare immediatamente l'operazione.

In casi più complessi tuttavia il delegato dell'utente potrebbe dover inviare una notifica al codice di libreria per segnalare l'esecuzione dell'annullamento. In tali casi, la modalità corretta per il delegato per terminare l'operazione consiste nel chiamare ThrowIfCancellationRequested(), che comporta la generazione di un evento OperationCanceledException. I nuovi overload di questa eccezione in .NET Framework versione 4 accettano un oggetto CancellationToken come argomento. Il codice di libreria può intercettare questa eccezione nel thread del delegato dell'utente ed esaminare il token dell'eccezione per determinare se l'eccezione indica l'annullamento cooperativo o un altro tipo di situazione particolare.

La classe Task gestisce l'oggetto OperationCanceledException in questo modo. Per ulteriori informazioni, vedere Annullamento delle attività.

Ascolto tramite polling

Per i calcoli a esecuzione prolungata eseguiti in ciclo o in modo ricorsivo, è possibile mettersi in ascolto di una richiesta di annullamento eseguendo periodicamente il polling del valore della proprietà CancellationToken.IsCancellationRequested. Se il valore è true, il metodo deve eseguire la pulizia e terminare il più rapidamente possibile. La frequenza ottimale di esecuzione del polling dipende dal tipo di applicazione. La frequenza di polling per un programma specifico deve essere determinata dallo sviluppatore. Il polling non ha un impatto significativo sulle prestazioni. Nell'esempio seguente viene illustrato un modo possibile per eseguire il polling.

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

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

Ascolto tramite registrazione di un callback

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

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

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

L'oggetto CancellationTokenRegistration gestisce la sincronizzazione dei thread e garantisce che l'esecuzione del callback verrà interrotta in un preciso momento.

Per garantire la capacità di risposta del sistema ed evitare deadlock, è necessario attenersi alle seguenti linee guida durante la registrazione dei callback:

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

  • Se si chiama Dispose durante l'esecuzione del callback e si mantiene un blocco su cui il callback è in attesa, è possibile che si verifichi il deadlock del programma. Una volta che Dispose restituisce un valore, è possibile liberare le risorse richieste dal callback.

  • Non è necessario che i callback eseguano thread manuali o utilizzino SynchronizationContext in un callback. Se un callback deve essere eseguito in un particolare thread, utilizzare il costruttore System.Threading.CancellationTokenRegistration che consente di specificare che il syncContext di destinazione è la proprietà SynchronizationContext.Current attiva. L'esecuzione del threading manuale in un callback può provocare un deadlock.

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

Ascolto tramite utilizzo di un handle di attesa

Quando un'operazione annullabile può bloccarsi mentre è in attesa in un primitiva di sincronizzazione come 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 del 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 la segnalazione è stata eseguita dal token di annullamento. L'operazione può quindi semplicemente terminare o generare un evento OperationCanceledException, in base alle esigenze.

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

Nel nuovo codice destinato a .NET Framework versione 4, sia System.Threading.ManualResetEventSlim che System.Threading.SemaphoreSlim supportano il nuovo framework di annullamento nei relativi metodi Wait. È possibile passare l'oggetto CancellationToken al metodo e, quando viene richiesto l'annullamento, l'evento si attiva e genera un evento OperationCanceledException.

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

Per un esempio più esaustivo, vedere Procedura: mettersi in ascolto di richieste di annullamento con handle di attesa.

Ascolto simultaneo di più token

In alcuni casi, è possibile che un listener debba mettersi in ascolto di più token di annullamento simultaneamente. Un'operazione annullabile potrebbe, ad esempio, 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 di token collegati che consente di unire due o più token in un unico token, come illustrato nell'esempio seguente.

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

Si noti che è necessario chiamare Dispose nell'origine di token collegati quando l'operazione è stata eseguita. Per un esempio più esaustivo, vedere Procedura: Ascolto di più richieste di annullamento.

Cooperazione tra codice di libreria e codice utente

Il framework di annullamento unificato consente al codice di libreria di annullare il codice utente e al codice utente di annullare il codice di libreria in modo cooperativo. Affinché la cooperazione avvenga senza problemi, è necessario che entrambe le parti rispettino le linee guida seguenti:

  • Se il codice di libreria fornisce operazioni annullabili, deve fornire anche 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 rappresentano esempi di classi che rispettano queste linee guida. Per ulteriori informazioni, vedere Annullamento delle attività e Procedura: annullare una query PLINQ.

Vedere anche

Altre risorse

Nozioni di base sul threading gestito