Condividi tramite


Utilizzo di metodi asincroni in ASP.NET MVC 4

di Rick Anderson

Questa esercitazione illustra le nozioni di base per la creazione di un'applicazione Web ASP.NET MVC asincrona 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 ASP.NET controller MVC 4 in combinazione con .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 l'attività. 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 è sintattica per indicare che un frammento di codice deve attendere in modo asincrono un'altra parte 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 che si abbia familiarità con il programma asincrono usando parole chiave await e asincrone e lo spazio dei nomi Task .

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

Modalità di esecuzione 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 elaborarla. 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 mancanza di risorse dei 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 per i nuovi inserimenti di thread. Se la concorrenza è bursty (ovvero, il sito Web può improvvisamente ottenere un numero elevato di richieste) e tutti i thread di richiesta disponibili sono occupati a causa di chiamate back-end con una latenza elevata, la velocità di inserimento dei thread limitata può rendere l'applicazione rispondere 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 usa metodi sincroni per gestire chiamate ad alta latenza in cui il pool di thread raggiunge il valore massimo predefinito .NET 4.5 di 5,000 thread utilizzerebbe circa 5 GB di memoria rispetto a un'applicazione in grado di gestire le stesse richieste usando metodi asincroni e solo 50 thread. Quando si esegue il 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'app Web che vede un numero elevato di richieste simultanee all'avvio o ha un carico bursty (in cui la concorrenza aumenta improvvisamente), l'esecuzione di chiamate al servizio Web aumenta la velocità di risposta dell'app. L'elaborazione di una richiesta asincrona ha la stessa durata 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. Di conseguenza, 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 dei metodi di azione sincroni o asincroni

In questa sezione vengono elencate le linee guida relative all'utilizzo dei 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 esecuzione breve.
  • La semplicità è più importante dell'efficienza.
  • Le operazioni sono principalmente operazioni della CPU anziché operazioni che comportano un sovraccarico esteso del disco o della rete. L'utilizzo di metodi di azione asincroni per operazioni associate alla CPU non fornisce alcun vantaggio e comporta un maggiore sovraccarico.

In generale, usare metodi asincroni per le condizioni seguenti:

  • Si chiamano i servizi che possono essere utilizzati tramite metodi asincroni e si usa .NET 4.5 o versione successiva.
  • Le operazioni sono associate alla rete o con vincoli di I/O anziché essere associate alla CPU.
  • Il parallelismo è più importante della semplicità del codice.
  • Si desidera 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. Se si effettua la chiamata asincrona, il thread di richiesta ASP.NET non viene bloccato mentre attende il completamento della richiesta del 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 utilizzare efficacemente 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 API Web ASP.NET metodi che a loro volta chiamano Task.Delay per simulare chiamate al 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 il migliore aumento dell'efficienza per la quantità di lavoro richiesta.

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. Effettua 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: il progetto Web Forms ASP.NET usato in un'altra esercitazione.

Metodo azione sincrona Gizmos

Il codice seguente mostra 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 API Web ASP.NET che restituisce un elenco di dati gizmos. Il progetto WebAPIpgw contiene l'implementazione dell'API gizmos, widget Web e product dei controller.
L'immagine seguente mostra la visualizzazione 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 del flusso di controllo sincrono di C#e il compilatore applica automaticamente le trasformazioni necessarie per usare i callback per evitare di bloccare i thread.

Il codice seguente illustra il Gizmos metodo sincrono e il GizmosAsync metodo asincrono. Se il browser supporta l'elemento HTML 5<mark>, verranno visualizzate 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 applicate le modifiche seguenti per consentire all'oggetto GizmosAsync di essere asincrono.

  • Il metodo è contrassegnato con la parola chiave asincrona , che indica al compilatore di generare callback per parti del corpo e di creare automaticamente un Task<ActionResult> oggetto 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 restituito di Task<ActionResult> rappresenta il lavoro in corso e fornisce ai chiamanti del metodo un handle tramite il quale attendere il completamento dell'operazione asincrona. In questo caso, il chiamante è il servizio Web. Task<ActionResult> rappresenta il lavoro in corso con un 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 verrà completato alla fine quando i dati sono disponibili. Poiché non si vuole eseguire altre operazioni finché non si dispone dei 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à. Esegue la registrazione del resto del metodo come callback nell'attività e restituisce immediatamente. Al termine dell'attività attesa, richiamerà tale callback e riprenderà quindi l'esecuzione del metodo a destra da dove è 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 mostra i metodi GetGizmos 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 al GizmosAsync precedente.

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

L'immagine seguente mostra la visualizzazione gizmo asincrona.

Async

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

Esecuzione di più operazioni in parallelo

I metodi di azione asincroni presentano un vantaggio significativo rispetto ai metodi sincroni quando un'azione deve eseguire diverse operazioni indipendenti. Nell'esempio fornito, il metodo PWGsincrono (per Products, Widgets 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 completamento del metodo asincrono PWGasync richiede poco più di 500 millisecondi 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 Task<ActionResult>che restituiscono sono annullabili, ovvero accettano un parametro CancellationToken quando ne viene fornito uno con l'attributo AsyncTimeout . Il codice seguente mostra 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 GetGizmosAsync, che accetta un parametro 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 chiama 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 dello 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 con IIS da un prompt dei comandi con privilegi elevati:
    %windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis -i
    Vedere ASP.NET iis Registration Tool (Aspnet_regiis.exe)

  • Potrebbe essere necessario aumentare il limite di HTTP.sys coda dal valore predefinito da 1.000 a 5.000. Se l'impostazione è troppo bassa, è possibile che venga visualizzatoHTTP.sys rifiutare le richieste con 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.
      Avanzate
    • Nella finestra di dialogo Impostazioni avanzate modificare Lunghezza coda da 1.000 a 5.000.
      Lunghezza 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 download di esempio.

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