Roberto Brunetti

ASP.NET - Mobility
Team System

SharePoint Conference

.NET Programming

Corsi

SharePoint

ASP.NET 2.0 Async Techniques - Parte 2

Nel precedente articolo abbiamo introdotto le problematiche di programmazione asincrona (e di conseguenza multithread) in ASP.NET, ripercorrendo passo per passo, con esempi semplici, i motivi per cui, effettuare chiamate asincrone, di per sè, peggiora le performance globali della nostra applicazione. Riporto il punto finale dell’articolo per proseguire con l’analisi della soluzione al problema. Ci siamo lasciati con una pagina sincrona (il default) che chiama due web service con il pattern asincrono Begin/EndMetodo. Questi erano i dati di performance con un carico di soli 20 client.

La pagina con la chiamata ad un web service che in sincrono impiegava 16,3 secondi passa con una chiamata asincrona a 21.6 secondi !!! e il processore passa dal 24% di carico al 29%; ottimo lavoro J abbiamo peggiorato e parecchio i tempi di risposta e nel contempo caricato di più la macchina.

Purtroppo questo esempio viene citato da molti come la soluzione al problema di parallelizzare le richieste "Se una pagina chiama un web service deve farlo in asincrono in modo da sfruttare i tempi morti": questo è quello che viene sbandierato da tanti.

Abbiamo invece dimostrato che questa soluzione peggiora i tempi di esecuzione totali della applicazione ! Il problema di fondo è che la chiamata a BeginMetodo impegna un altro thread del thread pool senza liberare il thread corrente, quindi, a fronte di una richiesta per la pagina che effettua una chiamata al web service, avremo 2 thread del thread pool impegnati e nella richiesta per la pagina che effettua due chiamate 3 thread del thread pool impegnati per tutta la durata della richiesta: questo è il motivo per cui i tempi di esecuzione totali con richieste concorrenti peggiorano le performance invece di migliorarle; pensate a 6 richieste concorrenti che fanno lavorare (6*3) 18 thread…esaurendo quasi i 20 thread a disposizione.

 

La soluzione al problema consiste nel rendere asincrona anche l'esecuzione della pagina da parte del motore di ASP.NET. Questa soluzione si può ottenere anche in ASP.NET 1.x implementando l'interfaccia IHttpAsyncHandler, ma perdendo molti degli automatismi di ASP.NET nell'uso dei controlli e nella gestione dello stato. In ASP.NET 2.0 ci hanno facilitato la vita tramite un attributo della pagina che consente in automatico di generare dietro le quinte l'implementazione di IHttpAsyncHandler.

Vi ricordo che all'indirizzo http://thinkmobile.it:9000/async ho pubblicato tutte le demo (anche quelle del precedente articolo) così potete provare da remoto le varie demo senza riscrivere il codice: il menù di sinistra vi porta ai vari esempi analizzati.

 

Partiamo da ASP.NET 1.x. Per rendere asincrona l’esecuzione di una pagina occorre implementare IAsyncHttpHandler. Ecco il codice:

 

---- 05AsyncHandler.ashx ---

<%@ WebHandler Language="C#" Class="SyncHandler10" %>

 

using System;

using System.Threading;

using System.Web;

 

public class SyncHandler10 : IHttpAsyncHandler

{

    SalesmanManagerWS.SalesmanManagerWS ws;

    HttpContext ctx;

   

    IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object state)

    {

        ctx = context;

        ws = new SalesmanManagerWS.SalesmanManagerWS();

        ctx.Response.Output.Write("<p>Thread " + AppDomain.GetCurrentThreadId());

        return ws.BeginSalvaAgente("robertob", "Roberto Brunetti", cb, ws);

    }

 

    void IHttpAsyncHandler.EndProcessRequest(IAsyncResult asyncResult)

    {

        ctx.Response.Output.Write("<p>Thread " + AppDomain.GetCurrentThreadId());

        ctx.Response.Output.Write(((SalesmanManagerWS.SalesmanManagerWS)asyncResult.AsyncState).EndSalvaAgente(asyncResult).ToString());

    }

 

    void IHttpHandler.ProcessRequest(HttpContext context) {

        throw new InvalidOperationException();

    }

 

    bool IHttpHandler.IsReusable {

        get { return false; }

    }

}

 

 

Questa pagina, in realtà questo codice, sfrutta l’estensione .ashx che ci evita, in questo semplice esempio, di dover creare un Http Handler esterno e registrarlo nel web.config (tecnica consigliata nella maggior parte dei casi in produzione).

Come si nota dall’esempio, la nostra classe, implementa IHttpAsyncHandler e consente, tramite i due metodi BeginProcessRequest e EndProcessRequest di effettuare chiamate asincrone ai web service. In pratica, il motore di ASP.NET rende asincrona l’esecuzione dell’handler, liberando il thread corrente (rimettendolo nel Thread Pool) dopo la chiamata a BeginProcessRequest. Questo significa, appunto, non occupare un thread del thread pool durante la richiesta asincrona al nostro, ormai famoso, web service.

Anche se i tempi di esecuzione sotto stress migliorano notevolmente rispetto a quanto abbiamo visto nell’ultima parte dell’articolo precedente, ci perdiamo il bello di ASP.NET, cioè la possibilità di lavorare con i controlli e gli eventi a livello di pagina: occorre fare tutto da codice.

 

Un workaround che uso spesso è quello di cercare di mantenere asincrona l’esecuzione dell’handler cercando di evitare la codifica della risposta a manina.

Una prima soluzione è quella di implementare la chiamata asincrona dentro il Global.asax, inserire il risultato in una variabile del contesto http (HttpContext.Items[“Risultato”] ad esempio) per poi recuperare questo elemento da una normale pagina sincrona. Il problema di questo approccio è dover riempire il Global.asax di If e Switch per invocare i web service corretti solo per le richieste a pagine che utilizzano web service.

Un secondo approccio più elegante, di cui vediamo il codice, è invece creare un handler .ashx per ogni pagina che necessita di effettuare chiamate asincrone.

Ad esempio, la pagina 06AsyncContext.aspx avrà un corrispondente 06AsyncHanderContext.ashx. Nell’handler asincrono chiameremo (sempre in asincrono, ovviamente) il web service. Alla ricezione della risposta (EndMethod) mettiamo il risultato in una variabile del contesto http e eseguiamo una Redirect verso la pagina reale, che, a questo punto, può usare Web Control e eventi nel modo tradizionale e recuperare il risultato dalla variabile del contesto.

Ecco l’esempio:

 

<%@ WebHandler Language="C#" Class="SyncHandler10" %>

 

using System;

using System.Threading;

using System.Web;

 

public class SyncHandler10 : IHttpAsyncHandler

{

    SalesmanManagerWS.SalesmanManagerWS ws;

    HttpContext ctx;

   

    IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object state)

    {

        ctx = context;

        ws = new SalesmanManagerWS.SalesmanManagerWS();

        return ws.BeginSalvaAgente("robertob", "Roberto Brunetti", cb, ws);

    }

 

    void IHttpAsyncHandler.EndProcessRequest(IAsyncResult asyncResult)

    {

       

        ctx.Items["RisultatoWS"] = ((SalesmanManagerWS.SalesmanManagerWS)asyncResult.AsyncState).EndSalvaAgente(asyncResult).ToString();

        ctx.Server.Execute("~/06AsyncContext10.aspx");

    }

 

    void IHttpHandler.ProcessRequest(HttpContext context) {

        throw new InvalidOperationException();

    }

 

    bool IHttpHandler.IsReusable {

        get { return true; }

    }

}

 

L’handler esegue la chiamata a BeginSalvaAgente e dopo aver ricevuto il risultato in EndProcessRequest, lo inserisce in HttpContext con il nome di RisultatoWS. L’esecuzione viene poi rediretta sulla pagina con una Server.Execute. La pagina, a questo punto, può utilizzare Web Control e eventi e recuperare il risultato dal Context. Ecco il codice della pagina:

 

<%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="~/MasterPage.master" %>

<%@ MasterType VirtualPath="~/MasterPage.master" %>

 

<script runat="server">

 

    void Page_Load() {

        Master.AddTraceMessage("Salva Agente Async da Global.asax");

        Master.AddTraceMessage("Risultato " + Context.Items["RisultatoWS"].ToString());

       

    }

 

</script>

 

Ottimo ! Abbiamo reso asincrona l’esecuzione della pagina senza incasinarci più di tanto. Ricordatevi che i link dalle altre pagine non devono puntare a pagina.aspx ma verso pagina.ashx.

 

In ASP.NET 2.0 ci hanno facilitato notevolmente il compito rendendo trasparente l’implementazione di IHttpAsyncHandler. È sufficiente decorare una pagina con l’attributo Async=”true” per avviare questo processo. Un ulteriore “miglioramento” della versione 2.0 di .NET è un nuovo pattern per le chiamate asincrone. Vediamo entrambi questi aspetti nel codice seguente.

 

--- 09ChiamaUnWebService.aspx ---

<%@ Page Language="C#" Async ="true" MasterPageFile="~/MasterPage.master" %>

<%@ MasterType VirtualPath="~/MasterPage.master" %>

 

<script runat="server">

 

    void Page_Load() {

        Master.AddTraceMessage("SalvaAgente 2.0 Async");

        SalesmanManagerWS.SalesmanManagerWS ws = new SalesmanManagerWS.SalesmanManagerWS();

        // Nuova riga

        ws.SalvaAgenteCompleted += thisOnSalvaAgenteCompleted; // Non occre definire il delegate (fa lui)

        // Non servono più

        //IAsyncResult ar = ws.BeginSalvaAgente("robertob", "RobertoBrunetti", AgenteSalvato, ws);

        //ar.AsyncWaitHandle.WaitOne();

        ws.SalvaAgenteAsync("Robertob", "RobertoBrunetti");

 

        Master.AddTraceMessage("SalvaAgente 2.0 Fine Chiamata");

    }

   

 

    //Definizione più conforme alla normale gestione eventi

    // private void AgenteSalvato(IAsyncResult ar)

 

    private void OnSalvaAgenteCompleted(object sender, SalesmanManagerWS.SalvaAgenteCompletedEventArgs e)

    {

        // N.B. Typed Result (SalvaAgenteCompletedEventArgs)

        // N.B. Il thread che esegue questo codice non è detto che sia lo stesso del Page_Load

        Master.AddTraceMessage("SalvaAgente 2.0 Completed");

        // Via sto casino

        // SalesmanManagerWS.SalesmanManagerWS ws = (SalesmanManagerWS.SalesmanManagerWS)ar.AsyncState;

        // bool ris = ws.EndSalvaAgente(ar);

        bool ris = e.Result;

        Master.AddTraceMessage("Risultato " + ris.ToString());

    }

</script>

 

Nell’esempio è stato usato il nuovo attributo Async=”true”. Nel codice trovate commentate le righe utilizzate in precedenza con il pattern Begin/End Metodo. Come si può notare il codice si è semplificato notevolmente. La chiamata al Web Service si effettua tramite il MetodoAsync (nel nostro caso SalvaAgenteAsync; esiste un EventHandler OnSalvaAgenteCompleted che semplifica il recupero dei risultati. In questo nuovo pattern, infatti, il risultato è tipizzato (SalvaAgenteCompletedEventArgs viene definito in automatico alla creazione del proxy) e sicuramente il codice è più semplice da leggere rispetto all’utilizzo di IAsyncResult e AsyncState.

 

Sul sito trovate anche un esempio di chiamata a due web service, sia in parallelo che in sequenza (nel caso in cui la chiamata al secondo abbia bisogno di un parametro che deriva dall’esecuzione del primo) che ometto per brevità.

 

Tutte le chiamate asincrone devono essere effettuate prima dell’Async Point: questo nuovo termine indica il punto in cui il motore di ASP.NET sospende l’esecuzione del thread corrente e reinserisce il thread nel thread pool. Quando terminano le operazioni asincrone viene recuperato un thread del thread pool per inviare la risposta al client. In pratica l’esecuzione della pagina viene sospesa sull’async point se ci sono richieste asincrone in corso e ripresa al termine della loro esecuzione. Se sull’async point le operazioni asincrone sono già terminate (nel nostro caso il web service ha già restituito una risposta) il thread non viene sospeso ma prosegue la sua esecuzione. Questa è una tecnica veramente molto efficiente.

Dov’è l’Async Point ? Questo “punto” di esecuzione della pipeline di una pagina si trova in corrispondenza dell’evento PreRenderComplete: questo significa che le chiamate asincrone possono essere effettuate in tutti gli eventi della pagina che vengono scatenati prima di PreRenderComplete. In pratica è consentito effettuare una chiamata asincrona negli eventi Page_PreInit, Page_Init, Page_InitComplete, Page_PreLoad, Page_LoadComplete, Page_PreRender. Non è possibile effettuare chiamate asincrone (o meglio è possibile ma non possono beneficiare di una reale chiamata asincrona) dagli eventi Page_PreRenderComplete, Page_SaveState, Page_SaveStateComplete, Page_Render, Page_Unload.

 

Nell’esempio precedente abbiamo usato il nuovo pattern MetodoAsync al posto del Begin/EndMetodo. Ci sono però alcune limitazioni nel nuovo pattern: non tutti i componenti .NET implementano questo pattern: ad esempio non è possibile utilizzarlo con chiamate asincrone verso i componenti di ADO.NET 2.0 in quanto non implementano questo pattern. Di conseguenza, conviene prendere la mano con il pattern Begin/EndMetodo, anche se più complicato da leggere/scrivere in quanto disponibile per tutti i componenti .NET che consentono chiamate asincrone. Il metodo Begin/End, però ha una limitazione (anche nella versione 1.x), cioè non fa confluire nella parte End della chiamata (la callback) Impersonation, Culture e HttpContext.Current che vanno reimpostati. Il nuovo pattern MetodoAysnc, al contrario, passa Impersonation, Culture e HttpContext al metodo di callback.

Riassumendo MetodoAysnc è più semplice da usare, ha il risultato tipizzato e lavora sul metodo di callback con lo stesso criterio di Impersonation, Culture e sullo stesso HttpContext (HttpContext.Current), ma non è implementato in molti componenti del .NET Framework. Il metodo Begin/End, anche se è utilizzabile con tutti i componenti asincroni, complica un po’ il codice e non esegue il metodo di callback con gli stessi criteri di Impersonation e Culture e nello stesso contesto http. A voi la scelta.

 

Sul sito trovate le demo 09, 10, 11 e 12 sul pattern MetodoAsync/Completed e le demo 13 e 14 con il pattern Begin/End.

 

Riporto per completezza gli esempi in sequenza:

 

--- 10 Chiamata a due Web Service in sequenza ---

<%@ Page Language="C#" Async ="true" MasterPageFile="~/MasterPage.master" %>

<%@ MasterType VirtualPath="~/MasterPage.master" %>

 

<script runat="server">

 

    void Page_Load() {

        Master.AddTraceMessage("SalvaAgente 2.0 Async");

        SalesmanManagerWS.SalesmanManagerWS ws = new SalesmanManagerWS.SalesmanManagerWS();

        ws.SalvaAgenteCompleted += thisOnSalvaAgenteCompleted;

        ws.SalvaAgenteAsync("Robertob", "RobertoBrunetti");

        Master.AddTraceMessage("SalvaAgente 2.0 Fine Chiamata");

    }

   

 

 

    private void OnSalvaAgenteCompleted(object sender, SalesmanManagerWS.SalvaAgenteCompletedEventArgs e)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Completed");

        bool ris = e.Result;

        Master.AddTraceMessage("Risultato " + ris.ToString());

        Master.AddTraceMessage("SalvaAgente 2.0 Async");

 

        SalesmanManagerWS.SalesmanManagerWS ws = new SalesmanManagerWS.SalesmanManagerWS();

        ws.SalvaAgenteCompleted += new SalesmanManagerWS.SalvaAgenteCompletedEventHandler(thisOnSalvaAgenteCompleted2);

        ws.SalvaAgenteAsync("Robertob", "RobertoBrunetti");

        Master.AddTraceMessage("SalvaAgente 2.0 Fine Chiamata");       

    }

   

    private void OnSalvaAgenteCompleted2(object sender, SalesmanManagerWS.SalvaAgenteCompletedEventArgs e)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Completed");

        bool ris = e.Result;

        Master.AddTraceMessage("Risultato " + ris.ToString());

    }

</script>

 

 

--- 11 Chiamata a due Web Service in parallelo---

<%@ Page Language="C#" Async ="true" MasterPageFile="~/MasterPage.master" %>

<%@ MasterType VirtualPath="~/MasterPage.master" %>

 

<script runat="server">

 

    void Page_Load()

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Async");

        SalesmanManagerWS.SalesmanManagerWS ws = new SalesmanManagerWS.SalesmanManagerWS();

        ws.SalvaAgenteCompleted += new SalesmanManagerWS.SalvaAgenteCompletedEventHandler(thisOnSalvaAgenteCompleted);

        ws.SalvaAgenteAsync("Robertob", "RobertoBrunetti");

        Master.AddTraceMessage("SalvaAgente 2.0 Fine Chiamata");

       

        Master.AddTraceMessage("SalvaAgente 2.0 Async");

        SalesmanManagerWS.SalesmanManagerWS ws2 = new SalesmanManagerWS.SalesmanManagerWS();

        ws2.SalvaAgenteCompleted += new SalesmanManagerWS.SalvaAgenteCompletedEventHandler(this.OnSalvaAgenteCompleted);

        ws2.SalvaAgenteAsync("Robertob", "RobertoBrunetti");

        Master.AddTraceMessage("SalvaAgente 2.0 Fine Chiamata");

    }

   

    // N.B. I Completed vengono sincronizzati: non c'è bisogno di lock

    private void OnSalvaAgenteCompleted(object sender, SalesmanManagerWS.SalvaAgenteCompletedEventArgs e)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Completed");

        bool ris = e.Result;

        Master.AddTraceMessage("Risultato " + ris.ToString());

    }

</script>

 

--- 12 Chiamata a due web service in parallelo utilizzando Anonymous Method 2.0 ---

<%@ Page Language="C#" Async="true" MasterPageFile="~/MasterPage.master" %>

 

<%@ MasterType VirtualPath="~/MasterPage.master" %>

 

<script runat="server">

 

    void Page_Load()

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Async");

        SalesmanManagerWS.SalesmanManagerWS ws = new SalesmanManagerWS.SalesmanManagerWS();

        ws.SalvaAgenteCompleted += delegate(object sender, SalesmanManagerWS.SalvaAgenteCompletedEventArgs e)

        {

            Master.AddTraceMessage("SalvaAgente 2.0 Completed");

            bool ris = e.Result;

            Master.AddTraceMessage("Risultato " + ris.ToString());

        };

 

        ws.SalvaAgenteAsync("Robertob", "RobertoBrunetti");

        Master.AddTraceMessage("SalvaAgente 2.0 Fine Chiamata");

       

        Master.AddTraceMessage("SalvaAgente 2.0 Async");

        SalesmanManagerWS.SalesmanManagerWS ws2 = new SalesmanManagerWS.SalesmanManagerWS();

        ws2.SalvaAgenteCompleted += delegate(object sender, SalesmanManagerWS.SalvaAgenteCompletedEventArgs e)

        {

            Master.AddTraceMessage("SalvaAgente 2.0 Completed");

            bool ris = e.Result;

            Master.AddTraceMessage("Risultato " + ris.ToString());

        };

        ws2.SalvaAgenteAsync("Robertob", "RobertoBrunetti");

        Master.AddTraceMessage("SalvaAgente 2.0 Fine Chiamata");

    }

   

</script>

 

 

Priva di vedere il codice che segue il classico pattern Begin/End occorre segnalare che la pagina necessita di una nuova chiamata in cui si registrano i metodi che verranno eseguiti in asincrono. Il modo più semplice per informare ASP.NET 2.0 che deve invocare i nostri metodi asincroni è quello di chiamare il metodo AddOnPreRenderCompleteAsync passando come parametri il nostro metodo Begin e il nostro metodo End. Come si può vedere nell’esempio seguente il codice è molto semplice: nel Page_Load (o comunque prima del Page_PreRenderComplete) si chiama il metodo AddOnPreRenderCompleteAsync indicando i nostri classici BeginSalvaAgente e EndSalvaAgente.

 

--- 13 Chiamata Begin/End a un web service ---

<%@ Page Language="C#" Async="true" MasterPageFile="~/MasterPage.master" %>

<%@ MasterType VirtualPath="~/MasterPage.master" %>

 

<script runat="server">

   

    SalesmanManagerWS.SalesmanManagerWS ws;

 

    void Page_Load() {

        AddOnPreRenderCompleteAsync(new BeginEventHandler(this.BeginSalvaAgente),

                                    new EndEventHandler(this.EndSalvaAgente));

    }

 

    IAsyncResult BeginSalvaAgente(Object sender, EventArgs e, AsyncCallback cb, object state) {

        Master.AddTraceMessage("SalvaAgente 2.0 Begin");

        ws = new SalesmanManagerWS.SalesmanManagerWS();

        IAsyncResult ar = ws.BeginSalvaAgente("robertob", "RobertoBrunetti", cb, state);

        Master.AddTraceMessage("SalvaAgente 2.0 Fine Chiamata");

        return ar;

    }

 

    void EndSalvaAgente(IAsyncResult asyncResult) {

        Master.AddTraceMessage("SalvaAgente 2.0 End");

        bool ris = ws.EndSalvaAgente(asyncResult);

        Master.AddTraceMessage("Risultato " + ris.ToString());

    }

   

</script>

 

Ecco una chiamata a due web service in sequenza registrando entrambi i Begin/EndMetodo dal metodo AddOnPreRenderCompleteAsync.

 

--- 14 Chiamata a Due Web Service –

<%@ Page Language="C#" Async ="true" MasterPageFile="~/MasterPage.master" %>

<%@ MasterType VirtualPath="~/MasterPage.master" %>

 

<script runat="server">

   

    SalesmanManagerWS.SalesmanManagerWS ws;

    SalesmanManagerWS.SalesmanManagerWS ws2;

 

    void Page_Load() {

        AddOnPreRenderCompleteAsync(new BeginEventHandler(thisBeginSalvaAgente),

                                    new EndEventHandler(thisEndSalvaAgente));

 

        AddOnPreRenderCompleteAsync(new BeginEventHandler(thisBeginSalvaAgente2),

                                    new EndEventHandler(thisEndSalvaAgente2));

 

    }

 

    IAsyncResult BeginSalvaAgente(Object sender, EventArgs e, AsyncCallback cb, object state)

    {