Modello asincrono basato su attività (TAP) in .NET: Introduzione e panoramica

In .NET il modello asincrono basato su attività è il modello di progettazione asincrono consigliato per il nuovo sviluppo. Si basa sui tipi Task e Task<TResult> nello spazio dei nomi System.Threading.Tasks, che vengono usati per rappresentare operazioni asincrone.

Denominazione, parametri e tipi restituiti

TAP usa un singolo metodo per rappresentare l'inizio e il completamento di un'operazione asincrona. Tale comportamento è in contrasto sia con il modello di programmazione asincrona (APM o IAsyncResult) che con il modello asincrono basato su eventi (EAP). Il modello di programmazione asincrona richiede i metodi Begin ed End. Il modello asincrono basato su eventi richiede un metodo con il suffisso Async, oltre a uno o più eventi, i tipi delegati del gestore eventi e i tipi derivati da EventArg. I metodi asincroni in TAP includono il suffisso Async dopo il nome dell'operazione per i metodi che restituiscono tipi awaitable, ad esempio, Task, Task<TResult>, ValueTask e ValueTask<TResult>. Ad esempio, un'operazione Get asincrona che restituisce Task<String> può essere denominata GetAsync. Se si aggiunge un metodo TAP a una classe che contiene già il nome di un metodo EAP con il suffisso Async, usare invece il suffisso TaskAsync. Ad esempio, se la classe dispone già di un metodo GetAsync, usare il nome GetTaskAsync. Se un metodo avvia un'operazione asincrona, ma non restituisce un tipo awaitable, il nome dovrebbe iniziare con Begin, Start o un altro verbo per suggerire che questo metodo non restituisce o genera il risultato dell'operazione.  

Un metodo TAP restituisce System.Threading.Tasks.Task o System.Threading.Tasks.Task<TResult>, a seconda che il metodo sincrono corrispondente restituisca void o un tipo TResult.

I parametri di un metodo TAP devono corrispondere ai parametri della relativa controparte sincrona e devono essere forniti nello stesso ordine. Tuttavia, i parametri out e ref sono esclusi da questa regola e dovrebbero essere evitati completamente. Tutti i dati che dovrebbero venire restituiti da un parametro out o ref dovranno invece essere restituiti come parte di TResult restituito da Task<TResult> e usare una tupla o una struttura dei dati personalizzata per contenere più valori. Valutare anche l'opportunità di aggiungere un parametro CancellationToken anche se la controparte sincrona del metodo TAP non ne offre uno.

I metodi dedicati esclusivamente alla creazione, modifica o combinazione di attività (dove l'intento asincrono del metodo risulta chiaro nel nome del metodo o nel nome del tipo a cui appartiene il metodo) non devono seguire questo modello di denominazione. Tali metodi vengono spesso definiti combinatori. Esempi di combinatori includono WhenAll e WhenAny e sono illustrati nella sezione Utilizzo di combinatori incorporati basati su attività dell'articolo Utilizzo del modello asincrono basato su attività.

Per esempi relativi alle differenze di sintassi di TAP rispetto ai modelli di programmazione asincrona legacy, come il modello di programmazione asincrono (APM) e il modello asincrono basato su eventi (EAP), vedere Modelli di programmazione asincrona.

Avvio di un'operazione asincrona

Un metodo asincrono basato su TAP può eseguire una piccola quantità di lavoro in modo sincrono, ad esempio convalidare gli argomenti e avviare l'operazione asincrona, prima di restituire l'attività risultante. Le attività sincrone devono essere minime in modo che il metodo asincrono possa eseguire rapidamente la restituzione. I motivi di una restituzione rapida includono:

  • I metodi asincroni possono essere richiamati dai thread dell'interfaccia utente e le attività sincrone a esecuzione prolungata potrebbero compromettere la velocità di risposta dell'applicazione.

  • Più metodi asincroni possono essere avviati simultaneamente. Pertanto, le attività a esecuzione prolungata nella parte sincrona di un metodo asincrono possono ritardare l'avvio di altre operazioni asincrone, riducendo quindi i vantaggi della concorrenza.

In alcuni casi, la quantità di lavoro richiesta per completare l'operazione è inferiore alla quantità di lavoro richiesta per avviare un'operazione in modo asincrono. La lettura da un flusso in cui l'operazione di lettura può essere soddisfatta dai dati che sono già stati memorizzati nel buffer in memoria è un esempio di tale scenario. In tali casi, l'operazione può essere completata in modo sincrono e può restituire un'attività già completata.

Eccezioni

Un metodo asincrono dovrebbe generare un'eccezione utilizzabile da una chiamata al metodo asincrono solo in risposta a un errore di utilizzo. Gli errori di utilizzo non devono mai verificarsi nel codice di produzione. Ad esempio, se passando un riferimento Null (Nothing in Visual Basic) come uno degli argomenti del metodo si determina uno stato di errore (generalmente rappresentato da un'eccezione ArgumentNullException), è possibile modificare il codice chiamante per assicurarsi che un riferimento Null non venga mai passato. Per tutti gli altri errori, le eccezioni che si verificano quando un metodo asincrono è in esecuzione devono essere assegnate all'attività restituita, anche se il metodo asincrono viene completato in modo sincrono prima che l'attività venga restituita. In genere, un'attività contiene al massimo un'eccezione. Tuttavia, se l'attività rappresenta più operazioni, (ad esempio WhenAll), più eccezioni possono essere associate a una singola attività.

Ambiente di destinazione

Quando si implementa un metodo TAP, è possibile determinare quando si verifica l'esecuzione asincrona. È possibile scegliere di eseguire il carico di lavoro nel pool di thread, implementarlo mediante I/O asincrono (senza associazione a un thread per la maggior parte dell'esecuzione delle operazioni), eseguirlo in un thread specifico (come il thread UI) o usare il numero desiderato di contesti potenziali. Un metodo TAP può anche non avere alcun elemento da eseguire e restituire solo un Task che rappresenta l'occorrenza di una condizione in un'altra posizione nel sistema (ad esempio, un'attività che rappresenta i dati provenienti da una struttura di dati in coda).

Il chiamante del metodo TAP può smettere di attendere il completamento del metodo TAP rimanendo in attesa in modalità sincrona dell'attività risultante o potrebbe eseguire codice aggiuntivo (continuazione) al completamento dell'operazione asincrona. L'autore del codice di continuazione è in grado di controllare dove viene eseguito il codice. È possibile creare il codice di continuazione in modo esplicito, con i metodi della classe Task, (ad esempio ContinueWith), o in modo implicito, usando il supporto linguistico compilato sulla base delle continuazioni, (ad esempio await in C#, Await in Visual Basic, AwaitValue in F#).

Stato attività

La classe Task fornisce un ciclo di vita per le operazioni asincrone e il ciclo è rappresentato dall'enumerazione TaskStatus. Per supportare i casi estremi di tipi che derivano da Task e da Task<TResult> e per supportare la separazione della costruzione dalla pianificazione, la classe Task espone un metodo Start. Le attività create dai costruttori Task pubblici vengono definite attività inattive, poiché iniziano il ciclo di vita nello stato non pianificato Created e vengono pianificate solo quando Start viene chiamato su queste istanze.

Tutte le altre attività iniziano il ciclo di vita in uno stato attivo, ovvero le operazioni asincrone che rappresentano sono già state avviate e lo stato dell'attività è un valore di enumerazione diverso da TaskStatus.Created. Tutte le attività che vengono restituite dai metodi TAP devono essere attivate. Se un metodo TAP usa internamente il costruttore di un'attività per creare un'istanza dell'attività da restituire, tale metodo deve chiamare Start sull'oggetto Task prima della restituzione. I consumer di un metodo TAP possono presumere in modo sicuro che l'attività restituita sia attiva e non devono tentare di chiamare Start su alcun oggetto Task restituito da un metodo TAP. Se si chiama Start su un'attività attiva, viene generata un'eccezione InvalidOperationException.

Annullamento (facoltativo)

In TAP, l'annullamento è facoltativo sia per gli implementatori di metodi asincroni che i consumer di metodi asincroni. Se un'operazione consente l'annullamento, espone un overload del metodo asincrono che accetta un token di annullamento (istanza diCancellationToken). Convenzionalmente, al parametro viene assegnato il nome cancellationToken.

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

L'operazione asincrona esamina questo token per le richieste di annullamento. Se riceve una richiesta di annullamento, può scegliere di soddisfarla e annullare l'operazione. Se la richiesta di annullamento determina la fine anticipata del lavoro, il metodo TAP restituisce un'attività che termina nello stato Canceled; non esiste alcun risultato disponibile e non viene generata alcuna eccezione. Lo stato Canceled è considerato uno stato finale (o completato) per un task, insieme a Faulted e RanToCompletion. Pertanto, se un'attività è nello stato Canceled, la proprietà IsCompleted restituisce true. Quando un'attività viene completata nello stato Canceled, tutte le continuazioni registrate con l'attività vengono pianificate o eseguite, a meno che sia stata specificata un'opzione di continuazione come NotOnCanceled per escludere la continuazione. Qualsiasi codice in attesa in modo asincrono di un'attività annullata tramite l'utilizzo di funzionalità del linguaggio continua a essere eseguito ma riceve OperationCanceledException o un'eccezione derivata. Il codice bloccato in attesa in modo sincrono dell'attività tramite metodi come Wait e WaitAll continua anch'esso ad essere eseguito con un'eccezione.

Se un token di annullamento ha richiesto l'annullamento prima che venga chiamato il metodo TAP che accetta il token, il metodo TAP deve restituire un'attività Canceled. Tuttavia, se viene richiesto l'annullamento mentre l'operazione asincrona è in esecuzione, tale operazione non ha bisogno di accettare la richiesta di annullamento. L'attività restituita deve terminare nello stato Canceled solo se l'operazione termina come risultato della richiesta di annullamento. Se viene richiesto l'annullamento ma viene comunque prodotto un risultato o un'eccezione, l'attività deve terminare nello stato RanToCompletion o Faulted.

Per i metodi asincroni per i quali si vuole esporre la possibilità di annullarli, non si deve specificare un overload che non accetta un token di annullamento. Per i metodi che non possono essere annullati, non fornire gli overload che accettano un token di annullamento; ciò indica al chiamante se il metodo di destinazione è realmente annullabile. Il codice del consumer che non richiede l'annullamento può chiamare un metodo che accetta CancellationToken e fornisce None come valore dell'argomento. None è equivalente dal punto di vista funzionale all'oggetto predefinito CancellationToken.

Creazione di report sullo stato di avanzamento (facoltativo)

Alcune operazioni asincrone prevedono il vantaggio dell'invio di notifiche sullo stato di avanzamento; queste vengono in genere usate per aggiornare un'interfaccia utente con informazioni sullo stato di avanzamento dell'operazione asincrona.

In TAP, lo stato di avanzamento viene gestito mediante un'interfaccia IProgress<T>, che viene passata al metodo asincrono come un parametro in genere denominato progress. La fornitura dell'interfaccia dello stato di avanzamento quando viene chiamato il metodo asincrono consente di eliminare le race condition che derivano da un utilizzo non corretto (ovvero quando gestori eventi non registrati correttamente dopo l'inizio delle operazioni possono non rilevare gli aggiornamenti). Ancora più importante, l'interfaccia dello stato di avanzamento supporta varie implementazioni dello stato di avanzamento, in base a quanto determinato dal codice consumer. Ad esempio, il codice consumer potrebbe controllare solo l'ultimo aggiornamento dello stato di avanzamento o memorizzare nel buffer tutti gli aggiornamenti o ancora richiamare un'azione per ogni aggiornamento oppure verificare che venga eseguito il marshalling della chiamata a un particolare thread. Tutte queste opzioni possono essere realizzate tramite un'implementazione diversa dell'interfaccia, personalizzata in base ai specifici requisiti del consumer. Come per l'annullamento, le implementazioni TAP devono fornire un parametro IProgress<T> solo se l'API supporta le notifiche dello stato di avanzamento.

Ad esempio, se il metodo ReadAsync illustrato in precedenza in questo articolo può segnalare lo stato di avanzamento intermedio sotto forma di numero di byte letti fino a qual momento, il callback dello stato di avanzamento può essere un'interfaccia IProgress<T>:

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

Se un metodo FindFilesAsync restituisce un elenco di tutti i file che soddisfano un criterio di ricerca particolare, il callback dello stato di avanzamento può fornire una stima della percentuale di lavoro completato e il set corrente di risultati parziali. Potrebbe fornire queste informazioni con una tupla:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

Oppure un tipo di dati specifico dell'API:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

Nel secondo caso, il tipo di dati speciale in genere presenta il suffisso ProgressInfo.

Se le implementazioni TAP forniscono overload che accettano un parametro progress, devono consentire che l'argomento sia null, nel qual caso non viene segnalato alcuno stato di avanzamento. Le implementazioni TAP devono segnalare lo stato di avanzamento all'oggetto Progress<T> in modo sincrono. Ciò consente al metodo asincrono di fornire rapidamente lo stato di avanzamento. Consente inoltre al consumer dello stato di avanzamento di determinare come e dove gestire meglio le informazioni. Ad esempio, l'istanza dello stato di avanzamento può scegliere di effettuare il marshalling dei callback e generare eventi in un contesto di sincronizzazione acquisito.

Implementazioni di IProgress<T>

.NET fornisce la classe Progress<T>, che implementa IProgress<T>. La classe Progress<T> viene dichiarata nel modo seguente:

public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T>? ProgressChanged;  
}  

Un'istanza di Progress<T> espone un evento ProgressChanged, che viene generato ogni volta che l'operazione asincrona segnala un aggiornamento di stato. L'evento ProgressChanged viene generato sull'oggetto SynchronizationContext acquisito quando è stata creata l'istanza di Progress<T>. Se non è disponibile alcun contesto di sincronizzazione, viene usato un contesto predefinito destinato al pool di thread. Gestori possono essere registrati con questo evento. Un singolo gestore può inoltre essere fornito al costruttore Progress<T> per praticità, comportandosi esattamente come un gestore per l'evento ProgressChanged. Gli aggiornamenti dello stato di avanzamento vengono generati in modo asincrono per evitare di ritardare l'operazione asincrona mentre sono in esecuzione i gestori eventi. Un'altra implementazione IProgress<T> può scegliere di applicare semantiche differenti.

Scelta degli overload da fornire

Se un'implementazione TAP usa i parametri facoltativi CancellationToken e IProgress<T>, potrebbe richiedere fino a quattro overload:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Tuttavia, molte implementazioni TAP non offrono funzionalità né di annullamento né di segnalazione dello stato di avanzamento, pertanto richiedono un singolo metodo:

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

Se l'implementazione TAP supporta l'annullamento o lo stato di avanzamento ma non entrambi, può fornire due overload:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

Se l'implementazione TAP supporta sia l'annullamento che lo stato di avanzamento, può esporre tutti e quattro gli overload. Tuttavia, può fornire solo i seguenti due:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Per compensare le due combinazioni intermedie mancanti, gli sviluppatori possono passare None o un oggetto CancellationToken predefinito per il parametro cancellationToken e null per il parametro progress.

Se si prevede che ogni utilizzo del metodo TAP supporti l'annullamento o lo stato di avanzamento, è possibile omettere gli overload che non accettano il parametro pertinente.

Se si decide di esporre più overload per rendere facoltativi l'annullamento o lo stato di avanzamento, gli overload che non supportano l'annullamento o lo stato di avanzamento devono comportarsi come se passassero None per l'annullamento o null per lo stato di avanzamento all'overload che li supporta.

Posizione Descrizione
Modelli di programmazione asincrona Vengono illustrati i tre modelli per eseguire le operazioni asincrone: il modello asincrono basato su attività (TAP), il modello di programmazione asincrono (APM) e il modello asincrono basato su eventi (EAP).
Implementazione del modello asincrono basato su attività Vengono descritti i tre modi per implementare il modello asincrono basato su attività (TAP): tramite i compilatori C# e Visual Basic in Visual Studio, manualmente oppure mediante una combinazione dei primi due.
Utilizzo del modello asincrono basato su attività Viene descritto come usare le attività e i callback per ottenere l'attesa senza blocchi.
Interoperabilità con altri tipi e modelli asincroni Viene descritto come usare il modello asincrono basato su attività (TAP) per implementare il modello di programmazione asincrono (APM) e il modello asincrono basato su eventi (EAP).