Condividi tramite


Uso di metodi asincroni in ASP.NET MVC 4

di Rick Anderson

Questa esercitazione illustra le nozioni di base della creazione di un'applicazione Web MVC asincrona ASP.NET usando Visual Studio Express 2012 per Web, che è una versione gratuita di Microsoft Visual Studio. È anche possibile usare Visual Studio 2012.

Per questa esercitazione su GitHub è disponibile un esempio completo https://github.com/RickAndMSFT/Async-ASP.NET/

La classe controller MVC 4 ASP.NET in combinazione .NET 4.5 consente di scrivere metodi di azione asincroni che restituiscono un oggetto di tipo Task<ActionResult>. .NET Framework 4 ha introdotto un concetto di programmazione asincrona denominato Task e ASP.NET MVC 4 supporta Task. Le attività sono rappresentate dal tipo di attività e dai tipi correlati nello spazio dei nomi System.Threading.Tasks . .NET Framework 4.5 si basa su questo supporto asincrono con le parole chiave await e asincrone che rendono l'uso di oggetti Task molto meno complessi rispetto agli approcci asincroni precedenti. La parola chiave await è sintatticamente abbreviata per indicare che un frammento di codice deve attendere in modo asincrono su un altro frammento di codice. La parola chiave asincrona rappresenta un hint che è possibile usare per contrassegnare i metodi come metodi asincroni basati su attività. La combinazione di await, async e l'oggetto Task semplifica notevolmente la scrittura di codice asincrono in .NET 4.5. Il nuovo modello per i metodi asincroni è denominato Modello asincrono basato su attività (TAP). Questa esercitazione presuppone una certa familiarità con il programma asincrono usando parole chiave await e asincrone e lo spazio dei nomi Task .

Per ulteriori informazioni sull'uso delle parole chiave await e async e dello spazio dei nomi Task, vedere i riferimenti seguenti.

Modalità di elaborazione delle richieste da parte del pool di thread

Nel server Web, .NET Framework gestisce un pool di thread usati per gestire le richieste di ASP.NET. Quando arriva una richiesta, viene inviato un thread dal pool per elaborare tale richiesta. Se la richiesta viene elaborata in modo sincrono, il thread che elabora la richiesta è occupato durante l'elaborazione della richiesta e tale thread non può eseguire un'altra richiesta.

Questo potrebbe non essere un problema, perché il pool di thread può essere reso sufficientemente grande per contenere molti thread occupati. Tuttavia, il numero di thread nel pool di thread è limitato (il valore massimo predefinito per .NET 4.5 è 5.000). In applicazioni di grandi dimensioni con concorrenza elevata di richieste a esecuzione prolungata, tutti i thread disponibili potrebbero essere occupati. Questa condizione è nota come fame di thread. Quando viene raggiunta questa condizione, il server Web accoda le richieste. Se la coda delle richieste diventa piena, il server Web rifiuta le richieste con stato HTTP 503 (Server Troppo occupato). Il pool di thread CLR presenta limitazioni sulle iniezioni di nuovi thread. Se la concorrenza è bursty (ovvero, il sito Web può ottenere improvvisamente un numero elevato di richieste) e tutti i thread di richiesta disponibili sono occupati a causa di chiamate al backend con alta latenza, il tasso di iniezione limitato dei thread può far sì che l'applicazione risponda molto male. Inoltre, ogni nuovo thread aggiunto al pool di thread ha un sovraccarico (ad esempio 1 MB di memoria dello stack). Un'applicazione web che utilizza metodi sincroni per gestire chiamate con elevata latenza, dove il pool di thread raggiunge il valore massimo predefinito di .NET 4.5 di 5,000 thread, consumerebbe circa 5 GB di memoria in più rispetto a un'applicazione in grado di gestire le stesse richieste usando metodi asincroni e solo 50 thread. Quando si esegue un lavoro asincrono, non si usa sempre un thread. Ad esempio, quando si effettua una richiesta di servizio Web asincrona, ASP.NET non usa thread tra la chiamata al metodo asincrono e l'await. L'uso del pool di thread per gestire le richieste con una latenza elevata può causare un footprint di memoria elevato e un utilizzo insufficiente dell'hardware del server.

Elaborazione di richieste asincrone

In un'applicazione web che riceve un numero elevato di richieste simultanee all'avvio o ha un carico irregolare (in cui la concorrenza simultanea aumenta improvvisamente), rendere asincrone le chiamate al servizio web aumenta la reattività dell'app. Una richiesta asincrona richiede lo stesso tempo necessario per l'elaborazione di una richiesta sincrona. Se una richiesta effettua una chiamata al servizio Web che richiede due secondi per il completamento, la richiesta richiede due secondi se viene eseguita in modo sincrono o asincrono. Tuttavia, durante una chiamata asincrona, un thread non viene bloccato a rispondere ad altre richieste mentre attende il completamento della prima richiesta. Pertanto, le richieste asincrone impediscono l'accodamento delle richieste e la crescita del pool di thread quando sono presenti molte richieste simultanee che richiamano operazioni a esecuzione prolungata.

Scelta di metodi di azione sincroni o asincroni

Questa sezione elenca le linee guida per quando usare metodi di azione sincroni o asincroni. Queste sono solo linee guida; esaminare singolarmente ogni applicazione per determinare se i metodi asincroni contribuiscono alle prestazioni.

In generale, usare metodi sincroni per le condizioni seguenti:

  • Le operazioni sono semplici o a breve esecuzione.
  • La semplicità è più importante dell'efficienza.
  • Le operazioni sono principalmente operazioni della CPU anziché operazioni che comportano un sovraccarico di rete o disco esteso. L'uso di metodi di azione asincroni sulle operazioni associate alla CPU non offre alcun vantaggio e comporta un sovraccarico maggiore.

In generale, usare metodi asincroni per le condizioni seguenti:

  • Stai chiamando servizi che possono essere consumati attraverso metodi asincroni e stai usando .NET 4.5 o versione successiva.
  • Le operazioni sono associate alla rete o associate a I/O anziché associate alla CPU.
  • Il parallelismo è più importante della semplicità del codice.
  • Si vuole fornire un meccanismo che consente agli utenti di annullare una richiesta a esecuzione prolungata.
  • Quando il vantaggio del cambio di thread supera il costo del cambio di contesto. In generale, è consigliabile creare un metodo asincrono se il metodo sincrono attende il thread di richiesta ASP.NET mentre non esegue alcun lavoro. Effettuando la chiamata in modo asincrono, il thread di richiesta di ASP.NET non viene bloccato senza far nulla mentre attende il completamento della richiesta al servizio web.
  • Il test mostra che le operazioni di blocco sono un collo di bottiglia nelle prestazioni del sito e che IIS può eseguire più richieste usando metodi asincroni per queste chiamate di blocco.

L'esempio scaricabile mostra come usare in modo efficace i metodi di azione asincroni. L'esempio fornito è stato progettato per fornire una semplice dimostrazione della programmazione asincrona in ASP.NET MVC 4 usando .NET 4.5. L'esempio non deve essere un'architettura di riferimento per la programmazione asincrona in ASP.NET MVC. Il programma di esempio chiama i metodi Web API di ASP.NET, i quali chiamano Task.Delay per simulare chiamate di servizio Web a esecuzione prolungata. La maggior parte delle applicazioni di produzione non mostrerà tali vantaggi evidenti all'uso di metodi di azione asincroni.

Poche applicazioni richiedono che tutti i metodi di azione siano asincroni. Spesso, la conversione di alcuni metodi di azione sincroni in metodi asincroni fornisce l'aumento di efficienza ottimale per la quantità di lavoro necessaria.

Applicazione di esempio

È possibile scaricare l'applicazione di esempio dal https://github.com/RickAndMSFT/Async-ASP.NET/ sito GitHub . Il repository è costituito da tre progetti:

  • Mvc4Async: il progetto ASP.NET MVC 4 che contiene il codice usato in questa esercitazione. Esegue chiamate API Web al servizio WebAPIpgw .
  • WebAPIpgw: il progetto api Web MVC 4 ASP.NET che implementa i Products, Gizmos and Widgets controller. Fornisce i dati per il progetto WebAppAsync e il progetto Mvc4Async .
  • WebAppAsync: progetto Web Form ASP.NET usato in un'altra esercitazione.

Metodo azione sincrona Gizmos

Il codice seguente illustra il Gizmos metodo di azione sincrono usato per visualizzare un elenco di gizmos. Per questo articolo, un gizmo è un dispositivo meccanico fittizio.

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}

Il codice seguente illustra il GetGizmos metodo del servizio gizmo.

public class GizmoService
{
    public async Task<List<Gizmo>> GetGizmosAsync(
        // Implementation removed.
       
    public List<Gizmo> GetGizmos()
    {
        var uri = Util.getServiceUri("Gizmos");
        using (WebClient webClient = new WebClient())
        {
            return JsonConvert.DeserializeObject<List<Gizmo>>(
                webClient.DownloadString(uri)
            );
        }
    }
}

Il GizmoService GetGizmos metodo passa un URI a un servizio HTTP dell'API Web ASP.NET che restituisce un elenco di dati gizmos. Il progetto WebAPIpgw contiene l'implementazione dei controller product e dell'API Web gizmos, widget.
L'immagine seguente mostra la vista gizmos del progetto di esempio.

Gizmos

Creazione di un metodo di azione Gizmos asincrono

L'esempio usa le nuove parole chiave async e await (disponibili in .NET 4.5 e Visual Studio 2012) per consentire al compilatore di gestire le trasformazioni complesse necessarie per la programmazione asincrona. Il compilatore consente di scrivere codice usando i costrutti sincroni del flusso di controllo C#e il compilatore applica automaticamente le trasformazioni necessarie per usare i callback per evitare thread di blocco.

Il codice seguente illustra il Gizmos metodo sincrono e il GizmosAsync metodo asincrono. Se il browser supporta l'elemento HTML 5<mark>, vedrai le modifiche in GizmosAsync evidenziazione gialla.

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}
public async Task<ActionResult> GizmosAsync()
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", await gizmoService.GetGizmosAsync());
}

Sono state apportate le seguenti modifiche per consentire a GizmosAsync di essere asincrono/a.

  • Il metodo è contrassegnato con la parola chiave async, che indica al compilatore di generare callback per parti del codice e di creare automaticamente un Task<ActionResult> che viene restituito.
  • "Async" è stato aggiunto al nome del metodo. L'aggiunta di "Async" non è obbligatoria, ma è la convenzione durante la scrittura di metodi asincroni.
  • Il tipo restituito è stato modificato da ActionResult a Task<ActionResult>. Il tipo di ritorno di Task<ActionResult> indica il lavoro in corso e fornisce ai chiamanti del metodo un handle per attendere il completamento di un'operazione asincrona. In questo caso, il chiamante è il servizio Web. Task<ActionResult> rappresenta il lavoro in corso con il risultato di ActionResult.
  • La parola chiave await è stata applicata alla chiamata al servizio Web.
  • L'API del servizio Web asincrona è stata chiamata (GetGizmosAsync).

All'interno del corpo del GetGizmosAsync metodo viene chiamato un altro metodo GetGizmosAsync asincrono. GetGizmosAsync restituisce immediatamente un Task<List<Gizmo>> oggetto che si completerà quando i dati saranno disponibili. Poiché non si vuole eseguire altre operazioni finché non si hanno i dati gizmo, il codice attende l'attività (usando la parola chiave await ). È possibile usare la parola chiave await solo nei metodi annotati con la parola chiave async .

La parola chiave await non blocca il thread fino al completamento dell'attività. Registra il resto del metodo come callback per il compito e restituisce immediatamente. Al termine dell'attività attesa, richiamerà il callback e riprenderà quindi l'esecuzione del metodo esattamente dove era stata interrotta. Per altre informazioni sull'uso delle parole chiave await e async e dello spazio dei nomi Task , vedere i riferimenti asincroni.

Il codice seguente illustra i GetGizmos metodi e GetGizmosAsync .

public List<Gizmo> GetGizmos()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            webClient.DownloadString(uri)
        );
    }
}
public async Task<List<Gizmo>> GetGizmosAsync()
{
    var uri = Util.getServiceUri("Gizmos");
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

Le modifiche asincrone sono simili a quelle apportate in precedenza a GizmosAsync .

  • La firma del metodo è stata annotata con la parola chiave asincrona , il tipo restituito è stato modificato in Task<List<Gizmo>>e Async è stato aggiunto al nome del metodo.
  • La classe HttpClient asincrona viene usata invece della classe WebClient .
  • La parola chiave await è stata applicata ai metodi asincroni HttpClient .

La seguente immagine mostra la vista asincrona del gizmo.

Async

La presentazione dei dati gizmos nei browser è identica alla visualizzazione creata dalla chiamata sincrona. L'unica differenza è che la versione asincrona può essere più efficiente con carichi pesanti.

Esecuzione di più operazioni in parallelo

I metodi di azione asincroni hanno un vantaggio significativo rispetto ai metodi sincroni quando un'azione deve eseguire diverse operazioni indipendenti. Nell'esempio fornito, il metodo PWGsincrono (per Products, Widget e Gizmos) visualizza i risultati di tre chiamate al servizio Web per ottenere un elenco di prodotti, widget e gizmos. Il progetto API Web ASP.NET che fornisce questi servizi usa Task.Delay per simulare la latenza o chiamate di rete lente. Quando il ritardo è impostato su 500 millisecondi, il metodo asincrono PWGasync richiede poco più di 500 millisecondi per il completamento mentre la versione sincrona PWG richiede più di 1.500 millisecondi. Il metodo sincrono PWG è illustrato nel codice seguente.

public ActionResult PWG()
{
    ViewBag.SyncType = "Synchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );

    return View("PWG", pwgVM);
}

Il metodo asincrono PWGasync è illustrato nel codice seguente.

public async Task<ActionResult> PWGasync()
{
    ViewBag.SyncType = "Asynchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var widgetTask = widgetService.GetWidgetsAsync();
    var prodTask = prodService.GetProductsAsync();
    var gizmoTask = gizmoService.GetGizmosAsync();

    await Task.WhenAll(widgetTask, prodTask, gizmoTask);

    var pwgVM = new ProdGizWidgetVM(
       widgetTask.Result,
       prodTask.Result,
       gizmoTask.Result
       );

    return View("PWG", pwgVM);
}

L'immagine seguente mostra la visualizzazione restituita dal metodo PWGasync .

pwgAsync

Uso di un token di annullamento

I metodi di azione asincroni che Task<ActionResult>restituiscono sono annullabili, ovvero accettano un parametro CancellationToken quando ne viene fornito uno con l'attributo AsyncTimeout . Il codice seguente illustra il GizmosCancelAsync metodo con un timeout di 150 millisecondi.

[AsyncTimeout(150)]
[HandleError(ExceptionType = typeof(TimeoutException),
                                    View = "TimeoutError")]
public async Task<ActionResult> GizmosCancelAsync(
                       CancellationToken cancellationToken )
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos",
        await gizmoService.GetGizmosAsync(cancellationToken));
}

Il codice seguente mostra l'overload di GetGizmosAsync, che prende come parametro un CancellationToken.

public async Task<List<Gizmo>> GetGizmosAsync(string uri,
    CancellationToken cancelToken = default(CancellationToken))
{
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri, cancelToken);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

Nell'applicazione di esempio fornita, selezionando il collegamento Cancellation Token Demo viene chiamato il GizmosCancelAsync metodo e viene illustrato l'annullamento della chiamata asincrona.

Configurazione del server per chiamate al servizio Web a concorrenza elevata/latenza elevata

Per sfruttare i vantaggi di un'applicazione Web asincrona, potrebbe essere necessario apportare alcune modifiche alla configurazione del server predefinita. Tenere presente quanto segue durante la configurazione e il test di stress dell'applicazione Web asincrona.

  • Windows 7, Windows Vista e tutti i sistemi operativi client Windows hanno un massimo di 10 richieste simultanee. È necessario un sistema operativo Windows Server per visualizzare i vantaggi dei metodi asincroni con carico elevato.

  • Registrare .NET 4.5 in IIS dal prompt dei comandi con privilegi elevati:
    %windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis -i
    Consultare Strumento di registrazione IIS di ASP.NET (Aspnet_regiis.exe)

  • Potrebbe essere necessario aumentare il limite della coda di HTTP.sys dal valore predefinito da 1.000 a 5.000. Se l'impostazione è troppo bassa, potresti vedere HTTP.sys rifiutare le richieste con uno stato HTTP 503. Per modificare il limite di coda HTTP.sys:

    • Aprire Gestione IIS e passare al riquadro Pool di applicazioni.
    • Fare clic con il pulsante destro del mouse sul pool di applicazioni di destinazione e selezionare Impostazioni avanzate.
      Avanzato
    • Nella finestra di dialogo Impostazioni avanzate modificare Lunghezza coda da 1.000 a 5.000.
      Lunghezza della coda

    Nota nelle immagini precedenti, .NET Framework è elencato come v4.0, anche se il pool di applicazioni usa .NET 4.5. Per comprendere questa discrepanza, vedere quanto segue:

  • Se l'applicazione usa servizi Web o System.NET per comunicare con un back-end tramite HTTP, potrebbe essere necessario aumentare l'elemento connectionManagement/maxconnection . Per ASP.NET applicazioni, questa funzionalità è limitata dalla funzionalità autoConfig a 12 volte il numero di CPU. Ciò significa che in un quad-proc è possibile avere al massimo 12 * 4 = 48 connessioni simultanee a un endpoint IP. Poiché questo è associato a autoConfig, il modo più semplice per aumentare maxconnection in un'applicazione ASP.NET consiste nell'impostare System.Net.ServicePointManager.DefaultConnectionLimit a livello di codice nel metodo from Application_Start nel file global.asax . Per un esempio, vedere il file di esempio.

  • In .NET 4.5 il valore predefinito di 5000 per MaxConcurrentRequestsPerCPU deve essere corretto.