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 rappresentano le operazioni asincrone.

Denominazione, parametri e tipi restituiti

TAP usa un singolo metodo per rappresentare l'inizio e il completamento di un'operazione asincrona. Questo approccio è in contrasto con il modello di programmazione asincrona (APM o IAsyncResult) e con il modello asincrono basato su eventi (EAP). APM richiede Begin e End metodi. EAP richiede un metodo con il suffisso Async e uno o più eventi, tipi di delegato del gestore di eventi e tipi derivati da EventArg. I metodi asincroni in TAP includono il Async suffisso 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 asincrona Get che restituisce un Task<String> oggetto può essere denominata GetAsync. Se si aggiunge un metodo TAP a una classe che contiene già un nome di metodo EAP con il Async suffisso , usare invece il suffisso TaskAsync . Ad esempio, se la classe dispone già di un GetAsync metodo, usare il nome GetTaskAsync. Se un metodo avvia un'operazione asincrona ma non restituisce un tipo awaitable, il nome deve iniziare con Begin, Start o un altro verbo per suggerire che questo metodo non restituisce né lancia il risultato dell'operazione.

Un metodo TAP restituisce un System.Threading.Tasks.Task oggetto o un oggetto System.Threading.Tasks.Task<TResult>, in base al fatto che il metodo sincrono corrispondente restituisca void o un tipo TResult.

I parametri di un metodo TAP devono corrispondere ai parametri della controparte sincrona e devono essere specificati nello stesso ordine. Tuttavia, out e ref i parametri sono esentati da questa regola e devono essere evitati completamente. Tutti i dati restituiti da un out parametro o ref devono invece far parte dell'oggetto TResult restituito da Task<TResult>e devono usare una tupla o una struttura di dati personalizzata per contenere più valori. Prendere in considerazione anche l'aggiunta di un CancellationToken parametro anche se la controparte sincrona del metodo TAP non ne offre uno.

I metodi dedicati esclusivamente alla creazione, alla manipolazione o alla combinazione di attività (in cui la finalità asincrona del metodo è chiara 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 descritti nella sezione Uso dei combinatori predefiniti basati su attività di Task dell'articolo Utilizzo del modello asincrono basato su attività di Task.

Per esempi di differenze tra la sintassi TAP e la sintassi usata nei modelli di programmazione asincrona legacy, ad esempio il modello di programmazione asincrona (APM) e il modello asincrono basato su eventi ,vedere Modelli di programmazione asincrona.

Comportamento asincrono, tipi restituiti e denominazione

La async parola chiave non forza l'esecuzione asincrona di un metodo in un altro thread. Abilita await, e il metodo viene eseguito sincronicamente fino a raggiungere un elemento awaitable incompleto. Se il metodo non raggiunge un awaitable incompleto, può essere completato in modo sincrono.

Per la maggior parte delle API, preferisci questi tipi di ritorno:

  • Usare Task per le operazioni asincrone che non producono un valore.
  • Usare Task<TResult> per operazioni asincrone che producono un valore.
  • Usare ValueTask o ValueTask<TResult> solo quando le misurazioni mostrano una pressione di allocazione e quando i consumer possono gestire i vincoli di utilizzo aggiuntivi.

Mantenere prevedibile la denominazione TAP:

  • Utilizzare il suffisso Async per i metodi che restituiscono tipi attendibili.
  • Non accodare Async ai metodi sincroni.
  • Aggiungere il nuovo overload MethodNameAsync insieme al "MethodName" esistente. Non rimuovere o rinominare l'API sincrona. Mantenere entrambi consente ai chiamanti di eseguire la migrazione al proprio ritmo senza interruzioni.

Avvio di un'operazione asincrona

Un metodo asincrono basato su TAP può eseguire una piccola quantità di lavoro in modo sincrono, ad esempio la convalida degli argomenti e l'avvio dell'operazione asincrona, prima di restituire l'attività risultante. Mantenere al minimo il lavoro sincrono in modo che il metodo asincrono restituisca rapidamente. I motivi di un ritorno rapido includono:

  • È possibile richiamare metodi asincroni dai thread dell'interfaccia utente e qualsiasi lavoro sincrono a esecuzione prolungata potrebbe danneggiare la velocità di risposta dell'applicazione.
  • È possibile avviare più metodi asincroni contemporaneamente. Pertanto, qualsiasi lavoro a esecuzione prolungata nella parte sincrona di un metodo asincrono potrebbe ritardare l'avvio di altre operazioni asincrone, riducendo così i vantaggi della concorrenza.

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

Eccezioni

Un metodo asincrono deve generare un'eccezione direttamente dalla 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 si passa un riferimento Null (Nothing in Visual Basic) come uno degli argomenti del metodo provoca uno stato di errore (in genere 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, assegnare eccezioni che si verificano quando viene eseguito un metodo asincrono all'attività restituita, anche se il metodo asincrono viene completato in modo sincrono prima della restituzione dell'attività. In genere, un'attività contiene al massimo un'eccezione. Tuttavia, se l'attività rappresenta più operazioni ,ad esempio WhenAll, più eccezioni potrebbero essere associate a una singola attività.

Ambiente di destinazione

Quando si implementa un metodo TAP, è possibile determinare dove si verifica l'esecuzione asincrona. È possibile scegliere di eseguire il carico di lavoro nel pool di thread, implementarlo usando l'I/O asincrono (senza essere associato a un thread per la maggior parte dell'esecuzione dell'operazione), eseguirlo in un thread specifico (ad esempio il thread dell'interfaccia utente) o usare un numero qualsiasi di potenziali contesti. Un metodo TAP potrebbe anche non avere nulla da eseguire e potrebbe restituire semplicemente un Task oggetto che rappresenta l'occorrenza di una condizione altrove nel sistema, ad esempio un'attività che rappresenta i dati in arrivo in una struttura di dati in coda.

Il chiamante del metodo TAP può bloccare l'attesa del completamento del metodo TAP attendendo in modo sincrono l'attività risultante oppure può eseguire codice aggiuntivo (continuazione) al termine dell'operazione asincrona. L'autore del codice di continuazione ha il controllo sulla posizione in cui viene eseguito il codice. È possibile creare il codice di continuazione in modo esplicito, tramite metodi nella classe Task (ad esempio, ContinueWith) o in modo implicito usando il supporto del linguaggio basato sulle continuazioni (ad esempio, await in C#, Await in Visual Basic, AwaitValue in F#).

Stato attività

La Task classe fornisce un ciclo di vita per le operazioni asincrone e tale ciclo è rappresentato dall'enumerazione TaskStatus . Per supportare casi limite di tipi che derivano da Task e Task<TResult>, e per supportare la separazione della costruzione dalla pianificazione, la classe Task espone un metodo Start. Le attività create dai costruttori pubblici Task sono chiamate attività fredde, perché iniziano il loro 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à avviate e il relativo stato dell'attività è un valore di enumerazione diverso da TaskStatus.Created. Tutte le attività 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, il metodo TAP deve chiamare Start sull'oggetto Task prima di restituirlo. I consumatori di un metodo TAP possono presupporre che il task restituito sia attivo e non dovrebbero provare a chiamare Start su qualsiasi Task oggetto restituito da un metodo TAP. La chiamata Start su un'attività attiva comporta un'eccezione InvalidOperationException.

Per indicazioni sui problemi relativi alla durata e alla proprietà dei metodi "fire-and-forget" dopo l'attivazione dei task, vedere Mantenere attivi i metodi asincroni.

Annullamento (facoltativo)

In TAP l'annullamento è facoltativo sia per gli implementatori di metodi asincroni che per i consumatori di metodi asincroni. Se un'operazione consente l'annullamento, espone un overload del metodo asincrono che accetta un token di annullamento (CancellationToken istanza). Per convenzione, il parametro è denominato cancellationToken.

public static 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 monitora questo token per le richieste di annullamento. Se riceve una richiesta di annullamento, potrebbe scegliere di rispettare tale richiesta e annullare l'operazione. Se la richiesta di annullamento termina prematuramente, il metodo TAP restituisce un'attività che termina nello Canceled stato; non è disponibile alcun risultato e non viene generata alcuna eccezione. Lo Canceled stato viene considerato uno stato finale (completato) per un'attività, insieme agli Faulted stati e RanToCompletion . Pertanto, se un'attività è nello stato Canceled, la relativa 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 non sia stata specificata un'opzione di continuazione, come NotOnCanceled, per evitare la continuazione. Qualsiasi codice che aspetta in modo asincrono un'attività annullata tramite l'uso delle funzionalità del linguaggio continua a essere eseguito, ma riceve un'eccezione OperationCanceledException o derivata da essa. Codici bloccati in modo sincrono in attesa dell'attività tramite metodi come Wait e WaitAll continuano a funzionare nonostante un'eccezione.

Se un token di annullamento richiede l'annullamento prima che venga chiamato il metodo TAP che accetta tale token, il metodo TAP deve restituire un'attività Canceled . Tuttavia, se l'annullamento viene richiesto durante l'esecuzione dell'operazione asincrona, l'operazione asincrona non deve accettare la richiesta di annullamento. L'attività restituita deve terminare nello Canceled stato solo se l'operazione termina in seguito alla richiesta di annullamento. Se viene richiesto l'annullamento ma viene ancora prodotto un risultato o generata un'eccezione, l'attività deve terminare nello stato RanToCompletion o Faulted.

Per i metodi asincroni che vogliono esporre la possibilità di essere annullati in primo luogo, non è necessario fornire un overload che non accetta un token di annullamento. Per i metodi che non possono essere annullati, non fornire overload che accettano un token di annullamento; ciò consente di indicare al chiamante se il metodo di destinazione è effettivamente annullabile. Il codice consumer che non desidera l'annullamento può chiamare un metodo che accetta un CancellationToken e fornire None come valore dell'argomento. None è funzionalmente equivalente all'oggetto predefinito CancellationToken.

Creazione di report sullo stato di avanzamento (facoltativo)

Alcune operazioni asincrone traggono vantaggio dalla fornitura di notifiche di avanzamento. In genere, usare queste notifiche per aggiornare un'interfaccia utente con informazioni sullo stato di avanzamento dell'operazione asincrona.

In TAP gestire lo stato di avanzamento tramite l'interfaccia IProgress<T>. Passare questa interfaccia al metodo asincrono come parametro, in genere denominato progress. Quando si specifica l'interfaccia di avanzamento al momento della chiamata al metodo asincrono, è possibile eliminare le condizioni di gara che risultano da un utilizzo errato. Queste condizioni di gara si verificano quando i gestori eventi vengono registrati in modo non corretto dopo l'avvio dell'operazione e non vengono aggiornati. Più importante, l'interfaccia di stato supporta diverse implementazioni dello stato di avanzamento, come determinato dal codice di utilizzo. Ad esempio, il codice che utilizza potrebbe interessarsi solo all'aggiornamento dello stato di avanzamento più recente oppure potrebbe voler memorizzare nel buffer tutti gli aggiornamenti; eseguire un'azione per ogni aggiornamento; o controllare se la chiamata venga eseguita su un thread specifico. Tutte queste opzioni sono ottenibili usando implementazioni diverse dell'interfaccia, personalizzate in base alle esigenze del consumer specifico. Come per l'annullamento, le implementazioni TAP devono fornire un IProgress<T> parametro solo se l'API supporta le notifiche di stato.

Ad esempio, se il ReadAsync metodo descritto in precedenza in questo articolo può segnalare lo stato di avanzamento intermedio sotto forma di numero di byte letti finora, il callback di stato potrebbe essere un'interfaccia IProgress<T> :

public static 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 FindFilesAsync metodo restituisce un elenco di tutti i file che soddisfano un criterio di ricerca specifico, il callback di stato potrebbe fornire una stima della percentuale di lavoro completata e del set corrente di risultati parziali. Potrebbe fornire queste informazioni con un tipo di tupla:

public static 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))

o con un tipo di dati specifico per l'API:

public static 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))

In quest'ultimo caso, il tipo di dati speciale viene in genere suffisso con ProgressInfo.

Le implementazioni TAP che forniscono sovraccarichi che accettano un parametro progress devono permettere che l'argomento sia null. Se si passa null, non viene segnalato alcun avanzamento. Le implementazioni TAP devono segnalare lo stato di avanzamento all'oggetto Progress<T> in modo sincrono, che consente al metodo asincrono di fornire rapidamente lo stato di avanzamento. Consente inoltre al consumatore delle informazioni sullo stato di avanzamento di determinare come e dove gestire al meglio le informazioni. Ad esempio, l'istanza di avanzamento potrebbe scegliere di gestire i callback e generare eventi in un contesto di sincronizzazione acquisito.

Implementazioni di IProgress<T>

.NET fornisce la Progress<T> classe , che implementa IProgress<T>. La classe Progress<T> viene dichiarata come segue:

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 di ProgressChanged, che viene attivato ogni volta che l'operazione asincrona segnala un aggiornamento sul progresso. L'evento ProgressChanged viene generato sull'oggetto SynchronizationContext acquisito dall'istanza Progress<T> quando viene creata un'istanza. Se non è disponibile alcun contesto di sincronizzazione, viene usato un contesto predefinito destinato al pool di thread. È possibile registrare i gestori con questo evento. Per praticità, è anche possibile fornire un singolo handler al Progress<T> costruttore. Questo gestore si comporta esattamente come un gestore eventi per l'evento ProgressChanged . Gli aggiornamenti dello stato vengono generati in modo asincrono per evitare di ritardare l'operazione asincrona durante l'esecuzione dei gestori eventi. Un'altra IProgress<T> implementazione potrebbe scegliere di applicare semantica diversa.

Scelta degli overload da fornire

Se un'implementazione TAP usa sia i parametri facoltativi CancellationToken che facoltativi 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 forniscono funzionalità di annullamento o avanzamento, quindi richiedono un singolo metodo:

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

Se un'implementazione TAP supporta solo l'annullamento o solo lo stato di avanzamento, ma non entrambi contemporaneamente, potrebbe 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 un'implementazione TAP supporta sia l'annullamento sia il progresso, potrebbe presentare tutti e quattro i sovraccarichi. Tuttavia, potrebbe fornire solo i due elementi seguenti:

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 valore predefinito CancellationToken per il cancellationToken parametro e null per il progress parametro .

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

Se si decide di esporre più overload per rendere facoltativo 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 supporta questi parametri.