Scenari di programmazione asincrona

Se si hanno esigenze associate a I/O (ad esempio richiesta di dati da una rete, accesso a un database o la lettura e la scrittura in un file system), si può 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 manipolare callback o conformarsi a una libreria che supporti l'asincronia. 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 o Task<T> all'interno di un metodo async.
  • Per il codice associato alla CPU, si attende un'operazione avviata in un thread in background con il metodo Task.Run.

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. Anche se esistono modi in cui affrontare codice asincrono diverso da async e await, questo articolo è incentrato sui costrutti a livello di linguaggio.

Nota

In alcuni degli esempi seguenti System.Net.Http.HttpClient classe viene usata per scaricare alcuni dati da un servizio Web. L'oggetto s_httpClient usato in questi esempi è un campo statico della classe Program (controllare l'esempio completo):

private static readonly HttpClient s_httpClient = new();

Esempio associato a I/O: download di dati da un servizio Web

Potrebbe essere necessario scaricare dati da un servizio Web quando viene premuto un pulsante, ma non si vuole bloccare il thread dell'interfaccia utente. Può essere eseguito come segue:

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 associato alla CPU: esecuzione di 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 situazione è avviare un thread in background che esegua l'operazione con Task.Run, e attenderne il risultato tramite await. Ciò consentirà all'interfaccia utente di essere disponibile durante l'esecuzione dell'attività.

static DamageResult CalculateDamageDone()
{
    return new 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);
};

Questo codice esprime con precisione lo scopo dell'evento clic del pulsante, non è necessario gestire manualmente un thread in background e non blocca le funzionalità.

Operazioni eseguite in background

Sul lato degli elementi di C#, il compilatore trasforma il codice in una macchina a stati che tiene traccia di varie operazioni, ad esempio sospendere l'esecuzione quando viene raggiunto await e riprendere l'esecuzione quando un'operazione di background è stata completata.

Per gli utenti che amano la teoria, questa è un'implementazione del Modello futuro di asincronia.

Nozioni fondamentali da sapere

  • 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 le operazioni associate alla CPU e a I/O

I primi due esempi di questa guida hanno illustrato come usare async e await per operazioni associate ai I/O e alla CPU. È molto importante identificare se un processo da eseguire è associato a I/O o alla CPU perché ciò può influire notevolmente sulle prestazioni del codice e potrebbe causare un uso improprio 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 oneroso?

    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 l'operazione è associata alla CPU e si è interessati nella velocità di risposta, usare async e await, ma passare l'operazione a un altro thread conTask.Run. Se l'operazione è appropriata per parallelismo e concorrenza, è consigliabile usare anche la libreria Task Parallel Library.

È 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 dall'URL specificato 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.

[HttpGet, Route("DotNetCount")]
static public async 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;
}

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

Attesa per 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 include due metodi, Task.WhenAll e Task.WhenAny che consentono di scrivere codice asincrono che esegue un'attesa senza blocchi su più processi in background.

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

private static 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.

    return await Task.FromResult(new User() { id = userId });
}

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

Questo è un altro modo per scrivere il codice in maniera più concisa tramite LINQ:

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

Sebbene produca una quantità minore di codice, è necessario prestare molta 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(). L'esempio precedente usa Enumerable.ToArray per eseguire la query in modo eager e archiviare i risultati in una matrice. Ciò forza l'esecuzione e l'avvio dell'attività da parte del codice id => GetUserAsync(id).

Consigli e informazioni importanti

Con la programmazione asincrona, è necessario tenere presenti alcuni dettagli che possono impedire comportamenti imprevisti.

  • I metodi async devono avere una parola chiave await nel corpo, altrimenti non verranno eseguiti.

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

  • Aggiungere "async" come suffisso di ogni nome di metodo scritto.

    Questa è la convenzione usata in .NET per differenziare più facilmente i metodi sincroni dai metodi asincroni. Alcuni metodi non chiamati in modo esplicito dal codice, ad esempio un gestore di eventi o un metodo di controller del Web, non vengono necessariamente applicati. Poiché questi metodi non vengono chiamati in modo esplicito dal codice, non è importante denominarli in modo esplicito.

  • async void deve 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 metodo async void non possono essere rilevate al di fuori di tale metodo.
    • async void metodi sono difficili da testare.
    • async void metodi async void possono causare effetti collaterali seri se il chiamante non li prevede asincroni.
  • Prestare attenzione quando si usano le espressioni lambda asincrone in espressioni LINQ

    Le espressioni lambda in LINQ usano esecuzioni posticipate, ovvero il codice potrebbe essere eseguito in qualsiasi momento quando meno previsto. 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 molto efficaci, ma devono essere usati insieme con precauzione e in modo chiaro.

  • Scrivere codice che attende attività in modo non bloccante

    Il blocco del thread attuale come mezzo per attendere il completamento di un Task può comportare deadlock e thread di contesto bloccati e può richiedere una gestione degli errori più complessa. La tabella seguente offre indicazioni su come gestire l'attesa di attività in un modo non bloccante:

    Usare questo... Invece di questo Quando si vuole eseguire questa operazione...
    await Task.Wait oppure 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'usoValueTaskladdove 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 modificatore async restituisce un risultato memorizzato nella cache o viene completato in modo sincrono, le allocazioni aggiuntive possono diventare impegnative in termini di tempo nelle sezioni di codice critiche per le prestazioni. Possono diventare onerose se si verificano in cicli ridotti. Per ulteriori informazioni, consultare Tipi restituiti asincroni generalizzati.

  • Valutare l'opportunità di usareConfigureAwait(false)

    Una domanda comune è "quando devo usare il metodo Task.ConfigureAwait(Boolean) ?". 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 ulteriori informazioni su ConfigureAwait, consultare le domande frequenti su ConfigureAwait.

  • Scrivere codice con meno dettagli sullo stato

    È consigliabile non dipendere dallo stato di oggetti globali o dall'esecuzione di alcuni 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.

Esempio completo

Il codice seguente è il testo completo del file Program.cs per l'esempio.

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "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/xamarin"
    };

    private static void Calculate()
    {
        // <PerformGameCalculation>
        static DamageResult CalculateDamageDone()
        {
            return new 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>
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(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>
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine("Displaying data: ", stringData);
    }

    // <GetUsersForDataset>
    private static 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.

        return await Task.FromResult(new User() { id = userId });
    }

    private 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);
    }
    // </GetUsersForDataset>

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

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async 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>

    static async 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 = new int[] { 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/xamarin: 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.

Altre risorse