Programmazione asincrona

Se si dispone di esigenze associate a I/O, ad esempio la richiesta di dati da una rete, l'accesso a un database o la lettura e la scrittura in un file system, si vuole usare la programmazione asincrona. Si potrebbe anche usare codice associato alla CPU, ad esempio per eseguire un calcolo di spese, che rappresenta uno scenario importante per scrivere codice asincrono.

C# ha un modello di programmazione asincrona a livello di linguaggio, che consente di scrivere facilmente codice asincrono senza dover eseguire il juggle callback o conforme a una libreria che supporta asincrona. Questo modalità segue ciò che è noto come Task-based Asynchronous Pattern (TAP) (Modello asincrono basato sull'attività).

Panoramica del modello asincrono

Il nucleo della programmazione asincrona è costituito da oggetti Task e Task<T> che modellano le operazioni asincrone. Sono supportati dalle parole chiavi async e await. Il modello è piuttosto semplice nella maggior parte dei casi:

  • Per il codice associato a I/O, si attende un'operazione che restituisce un Task oggetto o Task<T> all'interno di un async metodo.
  • Per il codice associato alla CPU, si attende un'operazione avviata in un thread in background con il Task.Run metodo .

La parola chiave await è l'elemento cruciale. Restituisce il controllo al chiamante del metodo che esegue await ed è questo che in ultima analisi consente a un'interfaccia utente di essere reattiva o a un servizio di essere flessibile. Sebbene siano disponibili modi per approcciare il codice asincrono diverso da async e await, questo articolo è incentrato sui costrutti a livello di linguaggio.

Esempio associato a I/O: Scaricare i dati da un servizio Web

Potrebbe essere necessario scaricare alcuni dati da un servizio Web quando viene premuto un pulsante, ma non si vuole bloccare il thread dell'interfaccia utente. Può essere eseguita in questo modo usando la System.Net.Http.HttpClient classe:

private readonly HttpClient _httpClient = new HttpClient();

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 _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Il codice esprime la finalità (scaricare i dati in modo asincrono) senza impantanarsi nell'interagire con Task gli oggetti.

Esempio associato alla CPU: Eseguire un calcolo per un gioco

Si supponga di scrivere un gioco per dispositivi mobili in cui l'uso di un pulsante può causare danni a molti nemici visualizzati sullo schermo. L'esecuzione del calcolo del danno può essere molto onerosa e in questo modo il thread dell'interfaccia utente dà l'impressione che il gioco si arresti durante l'esecuzione del calcolo.

Il modo migliore per gestire questa operazione consiste nell'avviare un thread in background, che esegue il lavoro usando e attendere il risultato usando Task.Runawait. Ciò consente all'interfaccia utente di sentirsi fluida perché il lavoro viene eseguito.

private DamageResult CalculateDamageDone()
{
    // Code omitted:
    //
    // Does an expensive calculation and returns
    // the result of that calculation.
}

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

Questo codice esprime chiaramente la finalità dell'evento click del pulsante, non richiede la gestione manuale di un thread in background e lo fa in modo non bloccato.

Operazioni eseguite in background

Sul lato C# delle cose, il compilatore trasforma il codice in un computer di stato che tiene traccia di elementi come la resa dell'esecuzione quando viene await raggiunta un'esecuzione e la ripresa dell'esecuzione al termine di un processo in background.

Per l'inclinazione teorica, si tratta di un'implementazione del modello promise di asincronia.

Elementi chiave da comprendere

  • Il codice asincrono può essere usato per il codice associato a I/O e alla CPU, ma in modo diverso per ogni scenario.
  • Il codice asincrono usa Task<T> e Task, che sono costrutti usati per modellare le operazioni eseguite in background.
  • La parola chiave async trasforma un metodo in un metodo asincrono, che consente di usare la parola chiave await nel relativo corpo.
  • Quando la parola chiave await viene applicata, interrompe il metodo di chiamata e restituisce il controllo al chiamante fino al completamento dell'attività attesa.
  • await può essere usato solo all'interno di un metodo asincrono.

Riconoscere il lavoro associato a CPU e I/O

I primi due esempi di questa guida illustrano come usare async e per il lavoro associato a I/O e await associato alla CPU. È fondamentale che sia possibile identificare quando è necessario eseguire un processo è associato a I/O o a una CPU perché può influire notevolmente sulle prestazioni del codice e potrebbe potenzialmente causare errori di utilizzo di determinati costrutti.

Rispondere a queste due domande prima di scrivere il codice:

  1. Il codice deve "attendere" l'esecuzione di operazioni, ad esempio la ricezione di dati da un database?

    Se la risposta è "Sì", l'operazione è associata a I/O.

  2. Il codice eseguirà un calcolo costoso?

    Se la risposta è "Sì", l'operazione è associata alla CPU.

Se l'operazione è associata a I/O, usare async e awaitsenzaTask.Run. Non si deve usare la libreria Task Parallel Library.

Se il lavoro associato alla CPU è associato alla CPU e si preoccupa della velocità di risposta, usare async e await, ma generare il lavoro su un altro thread conTask.Run. Se il lavoro è appropriato per la concorrenza e il parallelismo, prendere in considerazione anche l'uso della libreria parallela attività.

È anche necessario valutare sempre l'esecuzione del codice. Ad esempio, ci si potrebbe trovare in una situazione in cui l'operazione associata alla CPU non è abbastanza onerosa confrontata al sovraccarico di commutazioni di contesto durante il multithreading. Ogni scelta presenta un compromesso ed è necessario selezionare il compromesso più adatto alla situazione.

Altri esempi

Gli esempi seguenti illustrano i diversi modi in cui è possibile scrivere codice asincrono in C#. Trattano scenari diversi molto comuni.

Estrarre dati da una rete

Questo frammento di codice scarica il codice HTML dalla home page in https://dotnetfoundation.org e conta il numero di volte in cui si verifica la stringa ".NET" nel codice HTML. Usa ASP.NET per definire un metodo controller API Web, che esegue questa attività e restituisce il numero.

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.

private readonly HttpClient _httpClient = new HttpClient();

[HttpGet, Route("DotNetCount")]
public async Task<int> GetDotNetCount()
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");

    return Regex.Matches(html, @"\.NET").Count;
}

Di seguito viene illustrato lo stesso scenario scritto per un'app di Windows universale, che esegue la stessa attività quando viene premuto un pulsante:

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(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.
    // This is important to do here, before the "await" call, so that 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 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;
}

Attendere il completamento di più attività

Ci si potrebbe trovare in una situazione in cui è necessario recuperare più elementi di dati allo stesso tempo. L'API Task contiene due metodi Task.WhenAll e Task.WhenAny, che consentono di scrivere codice asincrono che esegue un'attesa senza blocco su più processi in background.

Questo esempio illustra come è possibile acquisire dati User per un set di userId.

public async 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.
}

public static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Ecco un altro modo per scrivere questo metodo più breve, usando LINQ:

public async 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.
}

public static async Task<User[]> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Anche se è meno codice, prestare attenzione quando si combina LINQ con codice asincrono. Poiché LINQ usa un'esecuzione posticipata (lazy), le chiamate asincrone non verranno eseguite immediatamente come avviene in un ciclo foreach a meno che non si forzi la sequenza generata per l'iterazione con una chiamata a .ToList() o .ToArray(). Nell'esempio precedente viene Enumerable.ToArray usato per eseguire la query in modo ansioso e archiviare i risultati in una matrice. Che forza l'esecuzione del codice id => GetUserAsync(id) e l'avvio dell'attività.

Informazioni e consigli importanti

Con la programmazione asincrona sono disponibili alcuni dettagli da tenere presente che possono impedire comportamenti imprevisti.

  • asynci metodi devono avere un oggettoawaitparola chiave nel loro corpo o non restituiranno mai!

    Questo è importante da tenere presente. Se await non viene usato nel corpo di un async metodo, il compilatore C# genera un avviso, ma il codice viene compilato ed eseguito come se fosse un metodo normale. Questo è incredibilmente inefficiente, poiché il computer di stato generato dal compilatore C# per il metodo asincrono non esegue alcuna operazione.

  • Aggiungere "Async" come suffisso di ogni nome di metodo asincrono scritto.

    Si tratta della convenzione usata in .NET per distinguere più facilmente metodi sincroni e asincroni. Alcuni metodi che non sono chiamati in modo esplicito dal codice ,ad esempio i gestori eventi o i metodi del controller Web, non si applicano necessariamente. Poiché non vengono chiamati in modo esplicito dal codice, l'uso esplicito della denominazione non è importante.

  • async voiddeve essere usato solo per i gestori eventi.

    async void è l'unico modo per consentire ai gestori eventi asincroni di funzionare correttamente, poiché gli eventi non hanno tipi restituiti (quindi non possono usare Task e Task<T>). Qualsiasi altro uso di async void non segue il modello TAP e può essere difficile da usare, ad esempio:

    • Le eccezioni generate in un async void metodo non possono essere rilevate all'esterno di tale metodo.
    • async void i metodi sono difficili da testare.
    • async void i metodi possono causare effetti collaterali negativi se il chiamante non prevede che siano asincroni.
  • Prestare attenzione quando si usano le espressioni lambda asincrone in espressioni LINQ

    Le espressioni lambda in LINQ usano l'esecuzione posticipata, ovvero il codice potrebbe terminare l'esecuzione alla volta in cui non lo si prevede. L'introduzione delle attività di blocco in questa operazione produce facilmente un deadlock se il codice non è scritto in maniera corretta. L'annidamento di codice asincrono come questo può anche rendere più difficile la valutazione dell'esecuzione del codice. Async e LINQ sono potenti, ma devono essere usati con attenzione e chiaramente il più possibile.

  • Scrivere codice che attende attività in modo non bloccante

    Bloccando il thread corrente come mezzo per attendere che un Task completamento possa comportare deadlock e thread di contesto bloccati e può richiedere una gestione più complessa degli errori. La tabella seguente fornisce indicazioni su come gestire l'attesa delle attività in modo non bloccato:

    Opzione Invece di questo Quando si vuole fare questo...
    await Task.Wait o Task.Result Recuperare il risultato di un'attività in background
    await Task.WhenAny Task.WaitAny Attendere che un'attività sia completa
    await Task.WhenAll Task.WaitAll Attendere che tutte le attività siano complete
    await Task.Delay Thread.Sleep Attendere un periodo di tempo
  • Prendere in considerazione l'usoValueTaskove possibile

    La restituzione di un oggetto Task dai metodi asincroni può introdurre colli di bottiglia delle prestazioni in determinati percorsi. Task è un tipo di riferimento, quindi usarlo significa allocare un oggetto. Nei casi in cui un metodo dichiarato con il async modificatore restituisce un risultato memorizzato nella cache o viene completato in modo sincrono, le allocazioni aggiuntive possono diventare un costo di tempo significativo nelle sezioni critiche delle prestazioni del codice. Possono diventare onerose se si verificano in cicli ridotti. Per altre informazioni, vedere tipi restituiti asincroni generalizzati.

  • Prendere in considerazione l'usoConfigureAwait(false)

    Una domanda comune è "quando è consigliabile usare il Task.ConfigureAwait(Boolean) metodo?". Il metodo consente a un'istanza Task di configurare il relativo awaiter. Questa è una considerazione importante e impostarla erroneamente potrebbe avere implicazioni sulle prestazioni e persino deadlock. Per altre informazioni su , vedere le domande frequenti suConfigureAwait ConfigureAwait.

  • Scrivere codice con meno dettagli sullo stato

    Non dipendere dallo stato degli oggetti globali o dall'esecuzione di determinati metodi. È preferibile dipendere dai valori restituiti dei metodi. Perché?

    • Sarà più facile valutare il codice.
    • Sarà più facile testare il codice.
    • La combinazione di codice sincrono e asincrono è molto più semplice.
    • È possibile evitare completamente le race condition.
    • La dipendenza dai valori restituiti semplifica il coordinamento di codice asincrono.
    • (Extra) funziona particolarmente bene con l'inserimento di dipendenze.

È consigliabile raggiungere una completa o quasi completa trasparenza referenziale nel codice. In questo modo si verificherà una codebase prevedibile, testabile e gestibile.

Altre risorse