Condividi tramite


Scenari di programmazione asincrona

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:

private static readonly HttpClient s_httpClient = new();

Per altre informazioni, vedere il codice di esempio completo alla fine di questo articolo.

Esaminare i concetti sottostanti

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à:

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.

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

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.

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

È possibile scrivere codice simile per un'app di Windows universale ed eseguire l'attività di conteggio dopo la pressione di 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.
    // 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.

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

È possibile scrivere questo codice in modo più conciso usando LINQ:

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

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.

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/maui"
    };

    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/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.