L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Scenari di programmazione asincrona
27/03/2025
Se il codice implementa scenari associati a I/O per supportare le richieste di dati di rete, l'accesso al database o le operazioni di lettura/scrittura del file system, la programmazione asincrona è l'approccio migliore. È anche possibile scrivere codice asincrono per scenari associati alla CPU, ad esempio calcoli costosi.
C# ha un modello di programmazione asincrona a livello di linguaggio che consente di scrivere facilmente codice asincrono senza dover gestire i callback o adeguarsi a una libreria che supporti l'asincronia. Il modello segue ciò che è noto come modello asincrono basato su attività (TAP).
Esplorare il modello di programmazione asincrona
Gli oggetti Task e Task<T> rappresentano il nucleo della programmazione asincrona. Questi oggetti vengono usati per modellare le operazioni asincrone supportando le parole chiave async e await. Nella maggior parte dei casi, il modello è piuttosto semplice per gli scenari associati a I/O e associato alla CPU. All'interno di un metodo async:
codice associato a I/O avvia un'operazione rappresentata da un oggetto Task o Task<T> all'interno del metodo async.
codice associato alla CPU avvia un'operazione su un thread in background con il metodo Task.Run.
In entrambi i casi, un Task attivo rappresenta un'operazione asincrona che potrebbe non essere completata.
La parola chiave await è dove avviene la magia. Restituisce il controllo al chiamante del metodo che contiene l'espressione await e consente infine all'interfaccia utente di essere reattiva o a un servizio di essere elastico. Anche se esistono modi diversi dall'uso delle espressioni async e await per affrontare il codice asincrono, questo articolo è incentrato sui costrutti a livello di linguaggio.
Nota
Alcuni esempi presentati in questo articolo usano la classe System.Net.Http.HttpClient per scaricare i dati da un servizio Web. Nel codice di esempio l'oggetto s_httpClient è un campo statico di tipo Program classe:
Quando si implementa la programmazione asincrona nel codice C#, il compilatore trasforma il programma in una macchina a stati. Questo costrutto tiene traccia di varie operazioni e stato nel codice, ad esempio la resa dell'esecuzione quando il codice raggiunge un'espressione await e riprende l'esecuzione al termine di un processo in background.
In termini di teoria dell'informatica, la programmazione asincrona è un'implementazione del modello promise di asincronia.
Nel modello di programmazione asincrona sono disponibili diversi concetti chiave da comprendere:
È possibile usare il codice asincrono sia per il codice associato a I/O che per il codice associato alla CPU, ma l'implementazione è diversa.
Il codice asincrono usa gli oggetti Task<T> e Task come costrutti per modellare il lavoro che si svolge in background.
La parola chiave async dichiara un metodo come metodo asincrono, che consente di usare la parola chiave await nel corpo del metodo.
Quando si applica la parola chiave await, il codice sospende il metodo chiamato e restituisce il controllo al suo chiamante fino al completamento dell'attività.
È possibile usare l'espressione await solo in un metodo asincrono.
Esempio associato a I/O: Scaricare i dati dal servizio Web
In questo esempio, quando l'utente seleziona un pulsante, l'app scarica i dati da un servizio Web. Non si vuole bloccare il thread dell'interfaccia utente per l'app durante il processo di download. Il codice seguente esegue questa attività:
C#
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request// from the web service is happening.//// The UI thread is now free to perform other work.var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
Il codice esprime lo scopo (scaricando dati in modo asincrono) senza perdere tempo per interagire con gli oggetti Task.
Esempio di limitazione della CPU: Eseguire il calcolo del gioco
Nell'esempio successivo, un gioco mobile infligge danni a diversi agenti sullo schermo in risposta a un evento pulsante. L'esecuzione del calcolo dei danni può essere costosa. L'esecuzione del calcolo nel thread dell'interfaccia utente può causare problemi di visualizzazione e interazione dell'interfaccia utente durante il calcolo.
Il modo migliore per gestire l'attività consiste nell'avviare un thread in background per completare il lavoro con il metodo Task.Run. L'operazione fornisce un risultato utilizzando un'espressione await. L'operazione riprende al termine dell'attività. Questo approccio consente l'esecuzione senza problemi dell'interfaccia utente mentre il lavoro viene completato in background.
C#
static DamageResult CalculateDamageDone()
{
returnnew DamageResult()
{
// Code omitted://// Does an expensive calculation and returns// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()// performs its work. The UI thread is free to perform other work.var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Il codice esprime chiaramente l'intento dell'evento del pulsante Clicked. Non richiede la gestione manuale di un thread in background e completa l'attività in modo non bloccante.
Riconoscere gli scenari associati a CPU e I/O
Negli esempi precedenti viene illustrato come usare il modificatore async e l'espressione await per il lavoro associato a I/O e associato alla CPU. Un esempio per ogni scenario illustra il modo in cui il codice è diverso in base alla posizione in cui è associata l'operazione. Per prepararsi per l'implementazione, è necessario comprendere come identificare quando un'operazione è associata a I/O o associata alla CPU. La scelta dell'implementazione può influire notevolmente sulle prestazioni del codice e potenzialmente causare errori di utilizzo dei costrutti.
Prima di scrivere codice, è necessario porre due domande principali:
Domanda
Sceneggiatura
Implementazione
Il codice deve attendere un risultato o un'azione, ad esempio i dati di un database?
legato a I/O
Usa il modificatore async e l'espressione awaitsenza il metodoTask.Run.
Evitare di usare Task Parallel Library.
Il codice deve eseguire un calcolo costoso?
vincolato alla CPU
Usare il modificatore async e l'espressione await, ma generare il lavoro su un altro thread con il metodo Task.Run. Questo approccio riguarda la velocità di risposta della CPU.
Se l'operazione è appropriata per parallelismo e concorrenza, è consigliabile usare anche la libreria Task Parallel Library.
Misurare sempre l'esecuzione del codice. È possibile scoprire che il lavoro associato alla CPU non è sufficientemente costoso rispetto al sovraccarico dei commutatori di contesto durante il multithreading. Ogni scelta ha compromessi. Scegli il compromesso corretto per la tua situazione.
Esplorare altri esempi
Gli esempi in questa sezione illustrano diversi modi per scrivere codice asincrono in C#. Riguardano alcuni scenari che potrebbero verificarsi.
Estrarre dati da una rete
Il codice seguente scarica il codice HTML da un DETERMINATO URL e conta il numero di volte in cui la stringa ".NET" si verifica nel codice HTML. Il codice usa ASP.NET per definire un metodo controller API Web, che esegue l'attività e restituisce il conteggio.
Nota
Se si prevede di eseguire l'analisi del codice HTML nel codice di produzione, non usare le espressioni regolari. Usare invece una libreria di analisi.
C#
[HttpGet, Route("DotNetCount")]
staticpublicasync Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)// to accept another request, rather than blocking on this one.var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
È possibile scrivere codice simile per un'app di Windows universale ed eseguire l'attività di conteggio dopo la pressione di un pulsante:
C#
privatereadonly HttpClient _httpClient = new HttpClient();
privateasyncvoidOnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
// Capture the task handle here so we can await the background task later.var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.// It's important to do the extra work here before the "await" call,// so the user sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
// The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.// This action is what allows the app to be responsive and not block the UI thread.var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count;
DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
Attesa per il completamento di più attività
In alcuni scenari, il codice deve recuperare più parti di dati contemporaneamente. Le API Task forniscono metodi che consentono di scrivere codice asincrono che esegue un'attesa non bloccante su più processi in background:
Nell'esempio seguente viene illustrato come acquisire i dati dell'oggetto User per un insieme di oggetti userId.
C#
privatestaticasync Task<User> GetUserAsync(int userId)
{
// Code omitted://// Given a user Id {userId}, retrieves a User object corresponding// to the entry in the database with {userId} as its Id.returnawait Task.FromResult(new User() { id = userId });
}
privatestaticasync Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
returnawait Task.WhenAll(getUserTasks);
}
È possibile scrivere questo codice in modo più conciso usando LINQ:
Anche se si scrive meno codice usando LINQ, prestare attenzione quando si combina LINQ con codice asincrono. LINQ usa l'esecuzione posticipata (o differita). Le chiamate asincrone non vengono eseguite immediatamente come avviene in un ciclo foreach, a meno che non si forza l'iterazione della sequenza generata con una chiamata al metodo .ToList() o .ToArray(). In questo esempio viene usato il metodo Enumerable.ToArray per eseguire la query in modo rapido e archiviare i risultati in un array. Questo approccio forza l'esecuzione dell'istruzione id => GetUserAsync(id) e l'avvio dell'attività.
Esaminare le considerazioni per la programmazione asincrona
Con la programmazione asincrona, esistono diversi dettagli da tenere presente che possono impedire comportamenti imprevisti.
Usare "await" all'interno del corpo di un metodo "async()"
Quando si usa il modificatore async, è necessario includere una o più espressioni await nel corpo del metodo. Se il compilatore non rileva un'espressione await, il metodo non restituisce. Anche se il compilatore genera un avviso, il codice viene comunque compilato e il compilatore esegue il metodo . La macchina a stati generata dal compilatore C# per il metodo asincrono non esegue alcuna operazione, quindi l'intero processo è estremamente inefficiente.
Aggiungere il suffisso "Async" ai nomi dei metodi asincroni
La convenzione di stile .NET consiste nell'aggiungere il suffisso "Async" a tutti i nomi di metodo asincroni. Questo approccio consente di distinguere più facilmente tra metodi sincroni e asincroni. Alcuni metodi che non vengono chiamati in modo esplicito dal codice (ad esempio i gestori eventi o i metodi del controller Web) non si applicano necessariamente in questo scenario. Poiché questi elementi non vengono chiamati in modo esplicito dal codice, l'uso della denominazione esplicita non è importante.
Restituire 'async void' solo dai gestori di eventi
I gestori eventi devono dichiarare i tipi di ritorno void e non possono usare o restituire oggetti Task e Task<T> come fanno altri metodi. Quando si scrivono gestori di eventi asincroni, è necessario utilizzare il modificatore async su un metodo che restituisce void. Altre implementazioni dei metodi di restituzione di async void non seguono il modello TAP e possono presentare problemi.
Le eccezioni generate in un metodo async void non possono essere intercettate all'esterno di tale metodo
async void metodi sono difficili da verificare
Metodi async void possono causare effetti collaterali negativi se il chiamante non si aspetta che vengano eseguiti in modalità asincrona
Prestare attenzione quando si utilizzano le espressioni lambda asincrone in LINQ
È importante prestare attenzione quando si implementano espressioni lambda asincrone nelle espressioni LINQ. Le espressioni lambda in LINQ usano l'esecuzione posticipata, il che significa che il codice può essere eseguito in un momento imprevisto. L'introduzione di attività di blocco in questo scenario può causare facilmente un deadlock, se il codice non è scritto correttamente. Inoltre, l'annidamento del codice asincrono può anche rendere difficile ragionare sull'esecuzione del codice. Async e LINQ sono potenti, ma queste tecniche devono essere usate insieme il più attentamente e chiaramente possibile.
Eseguire le attività in modo non bloccante
Se il programma richiede il risultato di un'attività, scrivere codice che implementa l'espressione await in modo non bloccante. Il blocco del thread corrente come mezzo per attendere in modo sincrono il completamento di un elemento Task può comportare deadlock e thread di contesto bloccati. Questo approccio di programmazione può richiedere una gestione degli errori più complessa. La tabella seguente fornisce indicazioni su come accedere ai risultati delle attività in modo non bloccante:
Scenario di attività
Codice corrente
Sostituire con 'await'
Recuperare il risultato di un'attività in background
Task.Wait oppure Task.Result
await
Continua quando un'attività viene completata
Task.WaitAny
await Task.WhenAny
Continua quando tutte le attività sono completate
Task.WaitAll
await Task.WhenAll
Continua dopo un certo periodo di tempo
Thread.Sleep
await Task.Delay
Prendere in considerazione l'uso del tipo ValueTask
Quando un metodo asincrono restituisce un oggetto Task, è possibile introdurre colli di bottiglia delle prestazioni in determinati percorsi. Poiché Task è un tipo riferimento, un oggetto Task viene allocato dall'heap. Se un metodo dichiarato con il modificatore async restituisce un risultato memorizzato nella cache o viene completato in modo sincrono, le allocazioni aggiuntive possono accumulare costi di tempo significativi nelle sezioni critiche delle prestazioni del codice. Questo scenario può diventare costoso quando le allocazioni si verificano in cicli ristretti. Per ulteriori informazioni, consultare Tipi restituiti asincroni generalizzati.
Comprendere quando impostare "ConfigureAwait(false)"
Gli sviluppatori spesso chiedono quando usare il Task.ConfigureAwait(Boolean) booleano. Questa API consente a un'istanza di Task di configurare il contesto per la macchina a stati che implementa qualsiasi espressione await. Quando il valore booleano non è impostato correttamente, possono verificarsi prestazioni ridotte o deadlock. Per altre informazioni, vedere Domande frequenti su ConfigureAwait.
Scrivere codice con meno stato
Evitare di scrivere codice che dipende dallo stato degli oggetti globali o dall'esecuzione di determinati metodi. È preferibile dipendere dai valori restituiti dei metodi. La scrittura di codice con meno stato offre molti vantaggi:
Più facile da ragionare sul codice
Più facile testare il codice
Più semplice combinare codice asincrono e sincrono
In grado di evitare condizioni di race nel codice
Semplice da coordinare il codice asincrono che dipende dai valori restituiti
(Bonus) Funziona bene con l'inserimento delle dipendenze nel codice
È consigliabile raggiungere una completa o quasi completa trasparenza referenziale nel codice. Questo approccio comporta una codebase prevedibile, testabile e gestibile.
Esaminare l'esempio completo
Il codice seguente rappresenta l'esempio completo, disponibile nel file di esempio Program.cs.
C#
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;
classButton
{
public Func<object, object, Task>? Clicked
{
get;
internalset;
}
}
classDamageResult
{
publicint Damage
{
get { return0; }
}
}
classUser
{
publicbool isEnabled
{
get;
set;
}
publicint id
{
get;
set;
}
}
publicclassProgram
{
privatestaticreadonly Button s_downloadButton = new();
privatestaticreadonly Button s_calculateButton = new();
privatestaticreadonly HttpClient s_httpClient = new();
privatestaticreadonly IEnumerable<string> s_urlList = newstring[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/shows/net-core-101/what-is-net",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://dotnetfoundation.org",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
privatestaticvoidCalculate()
{
// <PerformGameCalculation>static DamageResult CalculateDamageDone()
{
returnnew DamageResult()
{
// Code omitted://// Does an expensive calculation and returns// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()// performs its work. The UI thread is free to perform other work.var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
// </PerformGameCalculation>
}
privatestaticvoidDisplayDamage(DamageResult damage)
{
Console.WriteLine(damage.Damage);
}
privatestaticvoidDownload(string URL)
{
// <UnblockingDownload>
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request// from the web service is happening.//// The UI thread is now free to perform other work.var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
// </UnblockingDownload>
}
privatestaticvoidDoSomethingWithData(object stringData)
{
Console.WriteLine($"Displaying data: {stringData}");
}
// <GetUsersForDataset>privatestaticasync Task<User> GetUserAsync(int userId)
{
// Code omitted://// Given a user Id {userId}, retrieves a User object corresponding// to the entry in the database with {userId} as its Id.returnawait Task.FromResult(new User() { id = userId });
}
privatestaticasync Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
returnawait Task.WhenAll(getUserTasks);
}
// </GetUsersForDataset>// <GetUsersForDatasetByLINQ>privatestaticasync Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
returnawait Task.WhenAll(getUserTasks);
}
// </GetUsersForDatasetByLINQ>// <ExtractDataFromNetwork>
[HttpGet, Route("DotNetCount")]
staticpublicasync Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)// to accept another request, rather than blocking on this one.var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
// </ExtractDataFromNetwork>staticasync Task Main()
{
Console.WriteLine("Application started.");
Console.WriteLine("Counting '.NET' phrase in websites...");
int total = 0;
foreach (string url in s_urlList)
{
var result = await GetDotNetCount(url);
Console.WriteLine($"{url}: {result}");
total += result;
}
Console.WriteLine("Total: " + total);
Console.WriteLine("Retrieving User objects with list of IDs...");
IEnumerable<int> ids = newint[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
var users = await GetUsersAsync(ids);
foreach (User? user in users)
{
Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
}
Console.WriteLine("Application ending.");
}
}
// Example output://// Application started.// Counting '.NET' phrase in websites...// https://learn.microsoft.com: 0// https://learn.microsoft.com/aspnet/core: 57// https://learn.microsoft.com/azure: 1// https://learn.microsoft.com/azure/devops: 2// https://learn.microsoft.com/dotnet: 83// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31// https://learn.microsoft.com/education: 0// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42// https://learn.microsoft.com/enterprise-mobility-security: 0// https://learn.microsoft.com/gaming: 0// https://learn.microsoft.com/graph: 0// https://learn.microsoft.com/microsoft-365: 0// https://learn.microsoft.com/office: 0// https://learn.microsoft.com/powershell: 0// https://learn.microsoft.com/sql: 0// https://learn.microsoft.com/surface: 0// https://dotnetfoundation.org: 16// https://learn.microsoft.com/visualstudio: 0// https://learn.microsoft.com/windows: 0// https://learn.microsoft.com/maui: 6// Total: 238// Retrieving User objects with list of IDs...// 1: isEnabled= False// 2: isEnabled= False// 3: isEnabled= False// 4: isEnabled= False// 5: isEnabled= False// 6: isEnabled= False// 7: isEnabled= False// 8: isEnabled= False// 9: isEnabled= False// 0: isEnabled= False// Application ending.
L'origine di questo contenuto è disponibile in GitHub, in cui è anche possibile creare ed esaminare i problemi e le richieste pull. Per ulteriori informazioni, vedere la guida per i collaboratori.
Feedback su
.NET
.NET
è un progetto di open source. Selezionare un collegamento per fornire feedback:
Progettare soluzioni end-to-end in Microsoft Azure per creare Funzioni di Azure, implementare e gestire app Web, sviluppare soluzioni che usano Archiviazione di Azure e altro ancora.