Uso di metodi asincroni in ASP.NET 4.5

di Rick Anderson

Questa esercitazione illustra le nozioni di base sulla creazione di un'applicazione asincrona di Web Forms ASP.NET usando Visual Studio Express 2012 per Web, ovvero una versione gratuita di Microsoft Visual Studio. È anche possibile usare Visual Studio 2012. Le sezioni seguenti sono incluse in questa esercitazione.

Viene fornito un esempio completo per questa esercitazione in
https://github.com/RickAndMSFT/Async-ASP.NET/ nel sito GitHub .

ASP.NET 4.5 Pagine Web in combinazione con .NET 4.5 consente di registrare metodi asincroni che restituiscono un oggetto di tipo Task. .NET Framework 4 ha introdotto un concetto di programmazione asincrona denominato Attività e ASP.NET 4.5 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 Attività molto meno complessi rispetto agli approcci asincroni precedenti. La parola chiave wait è sintattica per indicare che un pezzo di codice deve attendere in modo asincrono su un altro pezzo 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 le parole chiave await e asincrone e lo spazio dei nomi attività .

Per altre informazioni sull'uso delle parole chiave await e asincrone e dello spazio dei nomi Attività , 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 il servizio di 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 abbastanza grande per ospitare 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 questa condizione viene raggiunta, le richieste di accodamento del server Web. Se la coda della richiesta diventa completa, il server Web rifiuta le richieste con uno stato HTTP 503 (server troppo occupato). Il pool di thread CLR presenta limitazioni per i nuovi inserimenti di thread. Se la concorrenza è scoppiata , 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 del thread limitata può rendere l'applicazione risposta molto scarsa. 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 aumenta fino al massimo predefinito .NET 4.5 di 5, 000 thread utilizza 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 verrà usato alcun thread tra la chiamata al metodo asincrono e l'attesa. L'uso del pool di thread per le richieste di servizio con una latenza elevata può causare un footprint di memoria elevato e un utilizzo insufficiente dell'hardware del server.

Elaborazione di richieste asincrone

Nelle applicazioni Web che vedono un numero elevato di richieste simultanee all'avvio o hanno un carico scoppiato (in cui la concorrenza aumenta improvvisamente), rendendo le chiamate di servizio Web asincrone aumentano la velocità di risposta dell'applicazione. L'elaborazione di una richiesta asincrona ha la stessa durata di una richiesta sincrona. Ad esempio, se una richiesta effettua una chiamata al servizio Web che richiede due secondi per completare, la richiesta richiede due secondi se viene eseguita in modo sincrono o asincrono. Tuttavia, durante una chiamata asincrona, un thread non viene bloccato per 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 sincroni o asincroni

Questa sezione elenca le linee guida per quando usare metodi sincroni o asincroni. Queste sono solo linee guida; esaminare ogni applicazione singolarmente per determinare se i metodi asincroni consentono di ottenere 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'uso di metodi asincroni nelle operazioni associate alla CPU non offre vantaggi e comporta un sovraccarico maggiore.

In generale, usare metodi asincroni per le condizioni seguenti:

  • Si chiamano i servizi che possono essere usati 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 di cambiare thread supera il costo del commutatore di contesto. In generale, è consigliabile creare un metodo asincrono se il metodo sincrono blocca il thread di richiesta ASP.NET durante l'esecuzione di nessun lavoro. Eseguendo 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 illustra come usare metodi asincroni in modo efficace. L'esempio fornito è stato progettato per fornire una semplice dimostrazione della programmazione asincrona in ASP.NET 4.5. L'esempio non è destinato a essere un'architettura di riferimento per la programmazione asincrona in ASP.NET. Il programma di esempio chiama API Web ASP.NET metodi che a sua volta chiamano Task.Delay per simulare chiamate di servizio Web a esecuzione prolungata. La maggior parte delle applicazioni di produzione non mostrerà tali vantaggi ovvi per l'uso di metodi asincroni.

Alcune applicazioni richiedono che tutti i metodi siano asincroni. Spesso, convertendo alcuni metodi sincroni in metodi asincroni offre l'aumento migliore dell'efficienza 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:

  • WebAppAsync: progetto Web Forms ASP.NET che utilizza il servizio Web API WebAPIpwg. La maggior parte del codice per questa esercitazione proviene dal progetto.
  • WebAPIpgw: 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 .
  • Mvc4Async: il progetto ASP.NET MVC 4 che contiene il codice usato in un'altra esercitazione. Esegue chiamate API Web al servizio WebAPIpwg .

Pagina sincrona di Gizmos

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

public partial class Gizmos : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var gizmoService = new GizmoService();
        GizmoGridView.DataSource = gizmoService.GetGizmos();
        GizmoGridView.DataBind();
    }
}

Il codice seguente mostra 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 pagina gizmos dal progetto di esempio.

Screenshot della pagina Sync Gizmos Web browser che mostra la tabella di gizmos con i dettagli corrispondenti immessi nei controller API Web.

Creazione di una pagina Gizmos asincrona

L'esempio usa le nuove parole chiave asincrone 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 l'uso dei callback per evitare il blocco dei thread.

ASP.NET pagine asincrone devono includere la direttiva Page con l'attributo Async impostato su "true". Il codice seguente mostra la direttiva Page con l'attributo Async impostato su "true" per la pagina GizmosAsync.aspx .

<%@ Page Async="true"  Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosAsync.aspx.cs" Inherits="WebAppAsync.GizmosAsync" %>

Il codice seguente illustra il Gizmos metodo sincrono Page_Load e la GizmosAsync pagina asincrona. Se il browser supporta l'elemento del contrassegno> HTML 5<, verranno visualizzate le modifiche in GizmosAsync evidenziazione gialla.

protected void Page_Load(object sender, EventArgs e)
{
   var gizmoService = new GizmoService();
   GizmoGridView.DataSource = gizmoService.GetGizmos();
   GizmoGridView.DataBind();
}

La versione asincrona:

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcAsync));
}

private async Task GetGizmosSvcAsync()
{
    var gizmoService = new GizmoService();
    GizmosGridView.DataSource = await gizmoService.GetGizmosAsync();
    GizmosGridView.DataBind();
}

Sono state applicate le modifiche seguenti per consentire la GizmosAsync pagina come asincrona.

  • La direttiva Page deve avere l'attributo Async impostato su "true".
  • Il RegisterAsyncTask metodo viene usato per registrare un'attività asincrona contenente il codice che viene eseguito in modo asincrono.
  • Il nuovo GetGizmosSvcAsync metodo viene contrassegnato con la parola chiave asincrona , che indica al compilatore di generare callback per parti del corpo e di creare automaticamente un Task oggetto restituito.
  • "Async" è stato aggiunto al nome del metodo asincrono. L'aggiunta di "Async" non è obbligatoria, ma è la convenzione durante la scrittura di metodi asincroni.
  • Il tipo restituito del nuovo GetGizmosSvcAsync metodo è Task. Il tipo restituito di Task rappresenta il lavoro in corso e fornisce ai chiamanti del metodo un handle tramite il quale attendere il completamento dell'operazione asincrona.
  • 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 GetGizmosSvcAsync 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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

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 sincrona.
  • La parola chiave await è stata applicata al metodo asincrono GetAsyncHttpClient.

L'immagine seguente mostra la visualizzazione gizmo asincrona.

Screenshot della pagina del Web browser Async Gizmos che mostra la tabella di gizmos con i dettagli corrispondenti immessi nei controller API Web.

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.

Note su RegisterAsyncTask

I metodi associati a RegisterAsyncTask verranno eseguiti immediatamente dopo PreRender.

Se si usano direttamente eventi di pagina void asincroni, come illustrato nel codice seguente:

protected async void Page_Load(object sender, EventArgs e) {
    await ...;
    // do work
}

non si ha più il controllo completo su quando si eseguono gli eventi. Ad esempio, se sia un file aspx che un oggetto . Gli eventi definiti Page_Load dal master e uno o entrambi sono asincroni, l'ordine di esecuzione non può essere garantito. Si applica lo stesso ordine indeterminato per i gestori eventi (ad esempio async void Button_Click ).

Esecuzione di più operazioni in parallelo

I metodi asincroni presentano un vantaggio significativo rispetto ai metodi sincroni quando un'azione deve eseguire diverse operazioni indipendenti. Nell'esempio fornito, la pagina sincrona PWG.aspx(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 della pagina PWGasync.aspx asincrona richiede poco più di 500 millisecondi mentre la versione sincrona PWG richiede più di 1.500 millisecondi. La pagina PWG.aspx sincrona è illustrata nel codice seguente.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );
    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();

    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}", 
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

Il code-behind asincrono PWGasync è illustrato di seguito.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    RegisterAsyncTask(new PageAsyncTask(GetPWGsrvAsync));
    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}",
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

private async Task GetPWGsrvAsync()
{
    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
       );

    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();           
}

L'immagine seguente mostra la visualizzazione restituita dalla pagina PWGasync.aspx asincrona.

Screenshot della pagina del Web browser Widget asincroni, Prodotti e Gizmos che mostra le tabelle Widget, Products e Gizmos.

Uso di un token di annullamento

I metodi asincroni Taskche restituiscono sono annullabili, ovvero accettano un parametro CancellationToken quando ne viene fornito uno con l'attributo AsyncTimeout della direttiva Page . Il codice seguente mostra la pagina GizmosCancelAsync.aspx con un timeout di al secondo.

<%@ Page  Async="true"  AsyncTimeout="1" 
    Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosCancelAsync.aspx.cs" 
    Inherits="WebAppAsync.GizmosCancelAsync" %>

Il codice seguente mostra il file GizmosCancelAsync.aspx.cs .

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcCancelAsync));
}

private async Task GetGizmosSvcCancelAsync(CancellationToken cancellationToken)
{
    var gizmoService = new GizmoService();
    var gizmoList = await gizmoService.GetGizmosAsync(cancellationToken);
    GizmosGridView.DataSource = gizmoList;
    GizmosGridView.DataBind();
}
private void Page_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();

    if (exc is TimeoutException)
    {
        // Pass the error on to the Timeout Error page
        Server.Transfer("TimeoutErrorPage.aspx", true);
    }
}

Nell'applicazione di esempio fornita, selezionando il collegamento GizmosCancelAsync viene chiamata la pagina GizmosCancelAsync.aspx e viene illustrato l'annullamento (per timeout) della chiamata asincrona. Poiché il tempo di ritardo è compreso in un intervallo casuale, potrebbe essere necessario aggiornare la pagina un paio di volte per ottenere il messaggio di errore di timeout.

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, Window 8 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 usando il comando seguente:
    %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.
      Screenshot di Internet Information Services Manager che mostra il menu Impostazioni avanzate evidenziato con un rettangolo rosso.
    • Nella finestra di dialogo Impostazioni avanzate modificare Lunghezza coda da 1.000 a 5.000.
      Screenshot della finestra di dialogo Impostazioni avanzate che mostra il campo Lunghezza coda impostato su 1000 ed evidenziato con un rettangolo rosso.

    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:

  • Controllo delle versioni .NET e multitargeting : .NET 4.5 è un aggiornamento sul posto a .NET 4.0

  • Come impostare un'applicazione IIS o un pool di app per l'uso di ASP.NET 3.5 anziché 2.0

  • Versioni e dipendenze di .NET Framework

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

Autori di contributi