Condividi tramite


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, utilizzati per rappresentare 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 contrasta con il modello di programmazione asincrona (APM o IAsyncResult) e il modello di programmazione asincrona 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 o genera 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. I dati che sarebbero stati restituiti attraverso un parametro out o ref dovrebbero invece essere restituiti come parte di TResult restituito da Task<TResult>, e dovrebbero usare una tupla o una struttura 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 sono 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.

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. Il lavoro sincrono deve essere mantenuto al minimo in modo che il metodo asincrono possa restituire rapidamente. I motivi di un ritorno rapido includono:

  • I metodi asincroni possono essere richiamati 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 può essere completata in modo sincrono e può restituire un'attività già completata.

Eccezioni

Un metodo asincrono dovrebbe sollevare un'eccezione nella chiamata asincrona al metodo 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, le eccezioni che si verificano quando è in esecuzione un metodo asincrono devono essere assegnate all'attività restituita, anche se il metodo asincrono viene completato in modo sincrono prima che venga restituita l'attività. 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 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 può semplicemente restituire un Task oggetto che rappresenta l'occorrenza di una condizione in un'altra posizione 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 sulla Task classe (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à state avviate e il relativo stato 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 in modo sicuro che l'attività restituita sia attiva e non dovrebbero tentare di chiamare Start su qualsiasi Task oggetto restituito da un metodo TAP. La chiamata Start su un'attività attiva comporta un'eccezione InvalidOperationException.

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 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, può scegliere di rispettare tale richiesta e annullare l'operazione. Se la richiesta di annullamento comporta la chiusura prematura del lavoro, il metodo TAP restituisce un'attività che termina nello stato Canceled. Non è disponibile alcun risultato e non viene sollevata 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 ha richiesto 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 sovraccarichi che accettano un token di annullamento; ciò aiuta a segnalare al chiamante se il metodo di destinazione è effettivamente annullabile. Il codice consumer che non desidera l'annullamento può chiamare un metodo che accetta CancellationToken e fornisce 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 stato; questi vengono in genere usati per aggiornare un'interfaccia utente con informazioni sullo stato di avanzamento dell'operazione asincrona.

In TAP lo stato di avanzamento viene gestito tramite un'interfaccia IProgress<T> , che viene passata al metodo asincrono come parametro denominato in genere progress. Fornire l'interfaccia di avanzamento quando viene chiamato il metodo asincrono consente di eliminare le race condition risultanti da un utilizzo non corretto, ovvero quando i gestori eventi registrati in modo non corretto dopo l'avvio dell'operazione potrebbero perdere gli aggiornamenti. Più importante, l'interfaccia di stato supporta diverse implementazioni dello stato di avanzamento, come determinato dal codice di utilizzo. Ad esempio, il codice utilizzato può interessarsi solo all'aggiornamento dello stato di avanzamento più recente, può voler mettere in buffer tutti gli aggiornamenti, può voler richiamare un'azione per ogni aggiornamento, oppure può voler controllare se l'invocazione viene eseguita su un thread particolare. Tutte queste opzioni possono essere ottenute usando un'implementazione diversa dell'interfaccia, personalizzata 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 è in grado di segnalare lo stato intermedio sotto forma di numero di byte letti finora, il callback di stato potrebbe 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 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 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 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.

Se le implementazioni TAP forniscono overload che accettano un parametro progress, devono consentire che l'argomento sia null, nel qual caso 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 che è stato acquisito quando è stata istanziata l'istanza di Progress<T>. Se non è disponibile alcun contesto di sincronizzazione, viene usato un contesto predefinito destinato al pool di thread. I gestori (handler) possono essere registrati a questo evento. Un singolo gestore può anche essere fornito al Progress<T> costruttore per praticità ed 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 sia l'annullamento che 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 un'implementazione TAP supporta sia l'annullamento che l'avanzamento, può esporre tutti e quattro gli overload. Tuttavia, può 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 il progresso, è 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 tali overload.

Titolo Descrizione
Modelli di programmazione asincrona Introduce i tre modelli per l'esecuzione di 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à Descrive come implementare il modello asincrono basato su attività (TAP) in tre modi: usando i compilatori C# e Visual Basic in Visual Studio, manualmente o tramite una combinazione del compilatore e dei metodi manuali.
Utilizzo del modello asincrono basato su attività Descrive come usare le attività e i callback per ottenere il processo di attesa senza bloccare.
Interoperabilità con altri modelli e tipi asincroni Viene descritto come usare il modello asincrono basato su attività (TAP) per implementare il modello di programmazione asincrona (APM) e il modello asincrono basato su eventi (EAP).