Roberto Brunetti

Developing in the cloud

.NET Programming

Archives

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)

    {

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

    }

 

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

    {

        CompositeAsyncResult compositeAsyncResult = new CompositeAsyncResult(2, cb, state);

        Master.AddTraceMessage("SalvaAgente 2.0 Begin");

        ws2 = new SalesmanManagerWS.SalesmanManagerWS();

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

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

        return ar;

    }

 

    void EndSalvaAgente2(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 End");

        bool ris = ws2.EndSalvaAgente(asyncResult);

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

 

    }

   

</script>

 

In questo ultimo esempio le due chiamate sono sequenziali in quanto abbiamo IAsyncResult diversi. Nel caso in cui vogliate effettuare le due richieste in parallelo occorre qualche riga di codice in più per implementare un CompositeAsyncResult. Ecco il codice per crearlo e utilizzarlo nell’esempio 15. Questo codice, per semplicità, potete inserirlo nella directory App_Code:

 

using System;

using System.Threading;

 

public class CompositeAsyncResult : IAsyncResult {

    int _numberOfOperationsRemaining;

    volatile bool _allCompleted;

    volatile bool _completedSynchronously;

    AsyncCallback _callback;

 

    AsyncCallback _originalCallback;

    object _originalState;

 

    public CompositeAsyncResult(int numberOfOperations, AsyncCallback cb, object state) {

        _numberOfOperationsRemaining = numberOfOperations;

        _allCompleted = (numberOfOperations == 0);

        _callback = new AsyncCallback(this.OnOperationCompleted);

 

        _originalCallback = cb;

        _originalState = state;

 

        if (_allCompleted) {

            _completedSynchronously = true;

            _originalCallback(this);

        }

    }

 

    void OnOperationCompleted(IAsyncResult asyncResult) {

        if (Interlocked.Decrement(ref _numberOfOperationsRemaining) == 0) {

            _allCompleted = true;

            _completedSynchronously = asyncResult.CompletedSynchronously;

            _originalCallback(this);

        }

    }

 

    public AsyncCallback Callback {

        get { return _callback; }

    }

 

    bool IAsyncResult.IsCompleted {

        get { return _allCompleted; }

    }

 

    bool IAsyncResult.CompletedSynchronously {

        get { return _allCompleted && _completedSynchronously; }

    }

 

    object IAsyncResult.AsyncState {

        get { return _originalState; }

    }

 

    WaitHandle IAsyncResult.AsyncWaitHandle {

        get { return null; }

    }

}

 

Ed ecco l’esempio 15 che fa uso del CompositeAsyncResult:

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

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

 

<script runat="server">

   

    SalesmanManagerWS.SalesmanManagerWS ws;

    SalesmanManagerWS.SalesmanManagerWS ws2;

    IAsyncResult ar;

    IAsyncResult ar2;

 

    void Page_Load() {

        AddOnPreRenderCompleteAsync(new BeginEventHandler(this.BeginWork),

                                    new EndEventHandler(this.EndWork));

 

 

    }

 

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

    {

        CompositeAsyncResult compositeAsyncResult = new CompositeAsyncResult(2, cb, state);

        Master.AddTraceMessage("SalvaAgente 2.0 Begin");

        ws = new SalesmanManagerWS.SalesmanManagerWS();

        ar = ws.BeginSalvaAgente("robertob", "RobertoBrunetti", compositeAsyncResult.Callback, state);

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

       

        Master.AddTraceMessage("SalvaAgente 2.0 Begin");

        ws2 = new SalesmanManagerWS.SalesmanManagerWS();

        ar2 = ws2.BeginSalvaAgente("robertob", "RobertoBrunetti", compositeAsyncResult.Callback, state);

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

                       

        return compositeAsyncResult;

    }

 

    void EndWork(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 End");

        bool ris = ws.EndSalvaAgente(ar);

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

 

        Master.AddTraceMessage("SalvaAgente 2.0 End");

        ris = ws.EndSalvaAgente(ar2);

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

 

    }

 

   

</script>

 

Come si può notare si crea un CompositeAsyncResult che viene utilizzato per entrambe le chiamate, che, grazie a questo meccanismo, possono viaggiare in parallelo.

 

In tutti i casi mostrati, pensate, dando uno sguardo alla parte iniziale di questo articolo, a quanto sia più semplice il codice della versione 2.0 di ASP.NET rispetto all’implementazione di un Http Handler asincrono, necessario nella versione 1.x per rendere più scalabili le soluzioni.

 

L’ultimo punto che andiamo a esaminare con questo articolo riguarda le Page Task. L’idea nasce dal fatto di semplificare pagine complesse che accedono a servizi esterni: si pensi ad un portale che visualizza previsioni del tempo, quotazioni di borsa, traffico autostradale, e così via. Se una di queste informazioni non è disponibile al momento della richiesta http, probabilmente ha più senso inviare comunque la pagina al cliente, senza ad esempio le previsioni del tempo, piuttosto che indicare all’utente che la pagina non è disponibile. Non ci sono problemi a cablare nel codice queste logiche, anche se richiedono uno sforzo non indifferente per cercare di sincronizzare le cose ed evitare lunghe attese. Con le Page Task diventa tutto più semplice seguendo questo “prontuario”:

1)     Una pagina deve impiegare al massimo un tempo X per essere inviata al cliente

2)     Tutte le richieste asincrone effettuate verso servizi esterni devono completarsi entro il tempo X

3)     Le richieste andate a buon fine serviranno per renderizzare parti della pagina

4)     Le richieste non andate a buon fine verranno contrassegnate sulla pagina con una indicazione di “mancato servizio”

 

Ecco un esempio: si registrano le varie attività asincrone da effettuare e si imposta il tempo massimo sulla pagina. Ogni attività (task) ha un BeginTask e un EndTask che consentono rispettivamente di iniziare la chiamata asincrona e di recuperare il risultato (come sempre) e un TimeOut che consente di indicare all’utente la mancata esecuzione di un servizio.

 

Ecco il codice autodescrittivo:

 

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

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

 

<script runat="server">

   

    SalesmanManagerWS.SalesmanManagerWS ws;

 

    void Page_Load()

    {

        PageAsyncTask task = new PageAsyncTask(

            new BeginEventHandler(this.BeginSalvaAgente),               // Evento Inizio

            new EndEventHandler(thisEndSalvaAgente),                   // Evento Fine

            new EndEventHandler(thisTimeoutSalvaAgente),               // Evento Timeout

            null,                                                       // State

            true);                                                      // ExecuteInParallel

 

        RegisterAsyncTask(task);

    }

 

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

    }

 

    void TimeoutSalvaAgente(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Timeout");

    }

   

</script>

 

Come prima cosa nel Page_Load andiamo a definire e registrare ogni singola task. Definiamo poi gli eventi per iniziare la chiamata (BeginSalvaAgente), per ricevere il risultato (EndSalvaAgente) e per gestire l’errore durante la chiamata (TimeoutSalvaAgente). Ovviamente potete scegliere i nomi che più vi piacciono

 

Durante la creazione di una PageAsyncTask si passano i tre eventi già descritti, l’eventuale State (nel nostro caso null) e true per effettuare richieste in parallelo, oppure false per effettuare richieste sequenziali. Ecco un esempio di due attività sequenziali:

 

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

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

 

<script runat="server">

   

    SalesmanManagerWS.SalesmanManagerWS ws;

    SalesmanManagerWS.SalesmanManagerWS ws2;

 

    void Page_Load() {

        PageAsyncTask task = new PageAsyncTask(

            new BeginEventHandler(this.BeginSalvaAgente),               // Evento Inizio

            new EndEventHandler(this.EndSalvaAgente),                   // Evento Fine

            new EndEventHandler(this.TimeoutSalvaAgente),               // Evento Timeout

            null,                                                       // State

            false);                                                      // ExecuteInParallel

 

        PageAsyncTask task2 = new PageAsyncTask(

            new BeginEventHandler(this.BeginSalvaAgente2),               // Evento Inizio

            new EndEventHandler(this.EndSalvaAgente2),                   // Evento Fine

            new EndEventHandler(this.TimeoutSalvaAgente),               // Evento Timeout

            null,                                                       // State

            false);                                                      // ExecuteInParallel

 

        RegisterAsyncTask(task);

        RegisterAsyncTask(task2);

 

    }

 

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

 

    }

 

    void TimeoutSalvaAgente(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Timeout");

    }

 

 

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

    {

        CompositeAsyncResult compositeAsyncResult = new CompositeAsyncResult(2, cb, state);

        Master.AddTraceMessage("SalvaAgente 2.0 Begin");

        ws2 = new SalesmanManagerWS.SalesmanManagerWS();

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

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

        return ar;

    }

 

    void EndSalvaAgente2(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 End");

        bool ris = ws2.EndSalvaAgente(asyncResult);

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

 

    }

   

</script>

 

Ecco un esempio di due attività in parallelo:

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

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

 

<script runat="server">

   

    SalesmanManagerWS.SalesmanManagerWS ws;

    SalesmanManagerWS.SalesmanManagerWS ws2;

 

    void Page_Load() {

        PageAsyncTask task = new PageAsyncTask(

            new BeginEventHandler(this.BeginSalvaAgente),               // Evento Inizio

            new EndEventHandler(this.EndSalvaAgente),                   // Evento Fine

            new EndEventHandler(this.TimeoutSalvaAgente),               // Evento Timeout

            null,                                                       // State

            true);                                                      // ExecuteInParallel

 

        PageAsyncTask task2 = new PageAsyncTask(

            new BeginEventHandler(this.BeginSalvaAgente2),               // Evento Inizio

            new EndEventHandler(this.EndSalvaAgente2),                   // Evento Fine

            new EndEventHandler(this.TimeoutSalvaAgente),               // Evento Timeout

            null,                                                       // State

            true);                                                      // ExecuteInParallel

 

        RegisterAsyncTask(task);

        RegisterAsyncTask(task2);

 

    }

 

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

 

    }

 

    void TimeoutSalvaAgente(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Timeout");

    }

 

 

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

    {

        CompositeAsyncResult compositeAsyncResult = new CompositeAsyncResult(2, cb, state);

        Master.AddTraceMessage("SalvaAgente 2.0 Begin");

        ws2 = new SalesmanManagerWS.SalesmanManagerWS();

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

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

        return ar;

    }

 

    void EndSalvaAgente2(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 End");

        bool ris = ws2.EndSalvaAgente(asyncResult);

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

 

    }

   

</script>

 

Come si nota l’unica differenza rispetto all’esempio precedente è nel valore true passato come ultimo parametro al costruttore della PageAsyncTask.

 

Un ultimo esempio di task con timeout:

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

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

 

<script runat="server">

   

    SalesmanManagerWS.SalesmanManagerWS ws;

    SalesmanManagerWS.SalesmanManagerWS ws2;

 

    void Page_Load() {

        PageAsyncTask task = new PageAsyncTask(

            new BeginEventHandler(this.BeginSalvaAgente),               // Evento Inizio

            new EndEventHandler(this.EndSalvaAgente),                   // Evento Fine

            new EndEventHandler(this.TimeoutSalvaAgente),               // Evento Timeout

            null,                                                       // State

            true);                                                      // ExecuteInParallel

 

        PageAsyncTask task2 = new PageAsyncTask(

            new BeginEventHandler(this.BeginSalvaAgente2),               // Evento Inizio

            new EndEventHandler(this.EndSalvaAgente2),                   // Evento Fine

            new EndEventHandler(this.TimeoutSalvaAgente),               // Evento Timeout

            null,                                                       // State

            true);                                                      // ExecuteInParallel

 

        RegisterAsyncTask(task);

        RegisterAsyncTask(task2);

 

    }

 

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

 

    }

 

    void TimeoutSalvaAgente(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 Timeout");

    }

 

 

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

    {

        CompositeAsyncResult compositeAsyncResult = new CompositeAsyncResult(2, cb, state);

        Master.AddTraceMessage("SalvaAgente 2.0 Begin");

        ws2 = new SalesmanManagerWS.SalesmanManagerWS();

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

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

        System.Threading.Thread.Sleep(2000);

        return ar;

    }

 

    void EndSalvaAgente2(IAsyncResult asyncResult)

    {

        Master.AddTraceMessage("SalvaAgente 2.0 End");

        bool ris = ws2.EndSalvaAgente(asyncResult);

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

 

    }

   

</script>

 

Vi ricordo, contrariamente a quanto indicato nell’help di ASP.NET 2.0, che il timeout si imposta come attributo della pagina e non può essere discriminato in base alla richiesta: in pratica il timeout indica, in secondi, “il tempo massimo per aspettare tutte le risposte” di tutte le task asincrone.

 

Una caratteristica interessante delle Page Task è la possibilità di lavorare sia su pagine asincrone (marcate con Async=”true”) sia su pagine sincrone (il default) senza modificare il codice. Ovviamente, se facciamo lavorare le Page Task su una pagina sincrona, l’esecuzione della pagina stessa sarà sincrona impiegando solo thread del thread pool.

 

Queste tecniche, come abbiamo avuto modo di dire più volte nel corso delle due parti in cui è stato diviso questo articolo, non si applicano solo alle richieste verso web service, ma anche agli altri componenti del framework che implementano uno dei due pattern (Begin-End o Async-Completed).

 

 

 

Roberto Brunetti

Roberto@DevLeap.it

Posted: feb 07 2007, 06:32 by rob | with 3 comment(s) |
Filed under:

Comments

Roberto Brunetti Mobile Blog said:

Per chi sviluppa mobile server-side e chi in generale lavora con ASP.NET 1.x o 2.0, ho pubblicato un

# febbraio 7, 2007 6:41

Roberto Brunetti said:

Riprendo quanto segnalato da Paolo nel primo paragrafo del suo ottimo sul problema della rientranza delle

# febbraio 7, 2007 7:47