Articoli DevLeap

Articoli DevLeap

February 2007 - Posts

ASP.NET 2.0 Async Techniques

Articolo pubblicato su Computer Programming nel 2006.

Questo articolo, diviso in due parti unite in questo testo, introduce le problematiche di programmazione asincrona (e di conseguenza multithread) in ASP.NET, ripercorrendo passo per passo le modalità disponibili in ASP.NET 1.x per arrivare alle novità della versione 2.0. Il flusso, anche se con meno dettagli per questioni di spazio, ripercorre gli argomenti dei corsi www.devleap.it su ASP.NET 1.x e 2.0 Core. Per ogni demo, l'articolo contiene il codice relativo che potete copiare direttamente; all'indirizzo www.thinkmobile.it:9000/Async ho pubblicato tutti gli esempi funzionanti live in modo che possiate vedere e verificare il tutto su un server Windows 2003 già configurato: a sinistra trovate un menù che porta alle varie pagine riportate in questo articolo.

 

Quando si parla di programmazione asincrona facciamo riferimento all'esecuzione di operazioni su thread differenti, parallelizzando diverse richieste a risorse remote in modo da annullare il più possibile i tempi di attesa su ciascuna di esse: iniziare un articolo con un frase criptica solitamente incuriosisce il lettore :-)

Proviamo a spiegarci con alcuni esempi partendo da un client Windows (o mobile) tradizionale per poi spostarci su ASP.NET. Il mio obiettivo è chiarire e semplificare, il più possibile, concetti che esistono da sempre nello sviluppo di applicazioni e calarli piano piano nella realtà web che, come sempre, differisce dallo sviluppo di applicazioni tradizionali. Forniamo esempi molto semplici e facilmente riproducibili. In questo primo articolo ci dilungheremo un po' anche su IIS, Thread Pool e Asynchronous Thread Queue (ATQ) per inquadrare bene la problematica e dare al lettore elementi concreti di valutazione, piuttosto che segnalare solamente le carattestistiche di ASP.NET senza inquadrarle nello scenario complessivo.

 

Supponiamo di avere un client Windows che deve effettuare due chiamate a web service remoti per ottenere alcune informazioni. Utilizzeremo per praticità un solo web service il cui codice .NET 2.0 è il seguente.

 

---- SalesmanManagerWS.asmx ---

<%@ WebService Language="C#" CodeBehind="~/App_Code/SalesmanManagerWS.cs" Class="SalesmanManagerWS" %>

 

--- SalesmanManagerWS.asmx.cs ---

using System;

using System.Web;

using System.Web.Services;

using System.Web.Services.Protocols;

using System.Threading;

 

[WebService(Namespace = "http://tempuri.org/")]

[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

public class SalesmanManagerWS : System.Web.Services.WebService

{

    public SalesmanManagerWS () {

 

        //Uncomment the following line if using designed components

        //InitializeComponent();

    }

 

    [WebMethod]

    public bool SalvaAgente(string idAgente, string descrizione)

    {

        Thread.Sleep(2000);

        try

        {

            SalesmanManager sm = new SalesmanManager();

            sm.AggiornaAgente(idAgente, descrizione);

            return true;

        }

        catch

        {

            return false;

        }

    }

   

}

 

Come si nota, il web service chiama una classe del Business Layer per salvare i dati di un agente nel DB. Per effettuare prove in locale simuliamo un lavoro di due secondi con una Thread.Sleep(2000). In pratica, visto che lavorando in locale si azzerano i tempi di connessione, imponiamo due secondi come tempo di esecuzione del metodo remoto. Nel ricreare l'esempio potete togliere la chiamata al mio Business Layer e adattare il Thread.Sleep ad un valore vicino al vostro scenario.

 

Costruendo il nostro client .NET, per semplicità, assegneremo al click su un bottone, la chiamata sincrona al metodo SalvaAgente:

 

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

bool ris = ws.SalvaAgente("robertob", "Roberto Brunetti");

 

Sicuramente la risposta non arriverà prima di due secondi visto che il web service internamente spende due secondi a vuoto. Durante questo tempo il thread che gestisce l'interfaccia utente (la Windows Form) resta bloccato in attesa di una risposta: non è possibile interagire con altri elementi del form e tantomeno con il form stesso (ad esempio minimizzare la finestra oppure spostarla).

 

Eseguire una chiama asincrona risolve il problema del blocco del thread principale consentendo all'utente di interagire con il form durante la chiamata. Dalla versione 1.0, il framework .NET mette a disposizione, sulle chiamate ai web service, un pattern che utilizza BeginMetodo e EndMetodo per iniziare una chiamata asincrona senza doversi preoccupare di creare e gestire thread da codice. In pratica la chiamata a BeginSalvaAgente crea, dietro le quinte, un secondo thread per l'esecuzione della richiesta remota. A tale metodo può essere passato un evento che verrà richiamato in automatico al termina della richiesta remota (Callback). Da questo evento è possibile ottenere il risultato della chiamata tramite il metodo EndSalvaAgente. In pratica nel metodo button_click si esegue la chiamata asincrona:

 

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

IAsyncResult ar = ws.BeginSalvaAgente("robertob", "Roberto Brunetti", AgenteSalvato, ws);

 

definendo poi un event handler per recuperare il risultato al termine della chiamata:

 

private void AgenteSalvato(IAsyncResult ar)

    {

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

        bool ris = ws.EndSalvaAgente(ar);

    }

 

La chiamata impiega sempre un tempo non inferiore ai due secondi, ma il thread principale non viene bloccato durante questa attesa. Non mi dilungo su questi metodi visto che sono presenti nel .NET Framework (e .NET Compact Framework) sino dal febbraio 2002, mese di uscita ufficiale del framework. Abbiamo quindi risolto, senza impazzire con il codice, il primo problema.

 

Complichiamo, si fa per dire, l'esempio richiamando due web service (nel nostro caso dimostrativo chiameremo sempre lo stesso metodo). Eseguendo due chiamate sincrone è ovvio che il tempo di risposta totale non possa essere inferiore a 4 secondi. Il thread resta bloccato per almeno due secondi per effettuare la prima chiamata e poi per almeno altri due secondi per aspettare la seconda risposta. In questo caso una chiamata (anzi due) asincrona al web service, non solo fa sì che il thread non resti bloccato, ma consente di parallelizzare le due richieste ottenendo un tempo di risposta notevolmente inferiore. Test sulla mia macchina evidenziano che la chiamata sincrona a un web service (a regime) impiega circa 2,016 secondi, la chiamata sincrona a due web service impiegano circa 4,016 secondi. Effettuando chiamate asicrone si ottiene un tempo di riposta di 2,016 per una sola richiesta e di 2,031 secondi per entrambe le richieste.

 

Facciamo il punto per poi spostarci nel mondo ASP.NET: nel caso di una sola richiesta, con una chiamata asincrona non possiamo migliorare il tempo di risposta in quanto il nostro web service impiega comunque almeno 2 secondi per essere eseguito; con una chiamata asincrona miglioriamo solamente l'interattività dell'utente con il form in quanto la chiamata viene spostata su un thread diverso da quello che gestisce la user interface (e non è poco). Nel caso di due chiamate (o più di due ovviamente) con le chiamate asincrone riusciamo a parallelizzare le richieste da più thread concorrenti (senza dover creare e gestire a mano i thread) abbassando il tempo di risposta totale (sempre che la linea lo permetta). Ad esempio se dobbiamo effettuare 3 richieste che normalmente impiegano rispettivamente 3 secondi, 4 secondi e 6 secondi, nel caso di chiamate sincrone il miglior risultato che possiamo ottenere è superiore ai 13 secondi, con chiamate asincrone possiamo arrivare fino a 6 secondi. Con chiamate sincrone avremo un solo thread in esecuzione, nel caso di chiamate asincrone avremo fino a 3 thread in contemporanea.

 

E' bene sottolineare da subito che l'utlizzo di più thread, di per se, peggiora i tempi di esecuzione in quanto carichiamo il sistema con la gestione di più thread che implica operazioni di thread switch; abbiamo però dimostrato come l'utlizzo di queste tecniche sia particolarmente efficace durante l'utilizzo di risorse remote in quanto si possono parallelizzare le richieste e diminuire i tempi morti di attesa nelle risposte.

 

Il pattern BeginMetodo/EndMetodo è disponibile nel .NET Framework 1.x per le chiamate ai web service (la classe proxy autogenerata da Visual Studio o con WSDL.exe contiene questa coppia di metodi per ogni metodo esposto dal web service), per i metodi esposti da molte classi del namespace System.IO e del namespace System.Net e nella versione 2.0 anche per accedere a SQL Server tramite i metodi della SqlClient. In pratica ovunque si debba lavorare con risorse remote come il file system, le risorse in rete e perchè no anche SQL Server.

 

Il .NET Framework 2.0 espone anche un nuovo e semplificato pattern per le chiamate asincrone, alternativo a Begin/End, che non richiede la definizione di IAsyncResult/AyncCallback. In pratica la chiamata si riassume nel codice seguente:

 

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

 

// Nuova riga

ws.SalvaAgenteCompleted += this.OnSalvaAgenteCompleted;

ws.SalvaAgenteAsync("Robertob", "Roberto Brunetti");

 

// Non occre definire il delegate (fa lui)

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

 

e l'evento di callback diventa:

 

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

{

    // N.B. Typed Result (SalvaAgenteCompletedEventArgs)

    // Via ar.AsyncState

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

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

      bool ris = e.Result;

}

 

Da notare come il risultato sia tipizzato, non occorrano cast rispetto alla classe proxy e quanto il codice sia più semplice da leggere rispetto all'esempio iniziale (le righe commentate rappresentano il codice Begin/End); sfortunatamente questo nuovo pattern non è disponibile per tutte le classi che implementano metodi asincroni: ha quindi senso capire e utilizzare anche la sintassi Begin/End in quanto per molte classi (fra cui i vari oggetti SqlClient) occorre ancora lavorare con questo pattern. Creare thread secondari consente anche di eseguire in asincrono operazioni di inizializzazione e start-up di form e applicazioni senza "rubare" tempo prezioso al thread principale durante le attese di risorse remote.

 

Inquadrate problematiche e soluzioni per un client Windows Form passiamo a ASP.NET che presenta una serie di problematiche diverse: non siamo più lato-client con una interfaccia "mono-user", ma operiamo server-side con diverse richieste in contemporanea. Ad ogni richiesta che arriva via Http viene assegnato un thread che recupera la risorsa da disco e la invia come risposta: questo compito viene assolto da Internet Information Service su macchine Windows. Nel caso in cui la richiesta sia per una risorsa gestita da ASP.NET, IIS tramite la ISAPI Application (o Extension che dir si voglia) ASPNET_ISAPI.DLL, ridirige la richiesta verso  il processo ASPNET_WP in IIS 5 o W3WP in IIS 6. Quando il motore di ASP.NET ha completato il suo lavoro, IIS invia la risposta al client che ha richiesto la risorsa.

 

IIS Thread Pool e Asynchronous Thread Queue

Immaginate cosa succederebbe se per ogni richiesta venisse creato un thread diverso: se sul sito arrivano 1.000 richieste concorrenti avremmo 1.000 thread concorrenti nel processo di Internet Information Service, "tutti il lotta fra di loro sullo stesso sistema" per recuperare le risorse e inviarle al client: come abbiamo accennato nella prima parte dell'articolo avere tanti thread significare caricare il processore per gestire il time slice di ogni thread, l'assegnazione delle risorse della macchina al thread in esecuzione e il thread switch per passare da un thread all'altro. Senza dilungarci, in pratica, il costo di gestione dei thread influirebbe in modo pesante sul tempo di esecuzione delle singole richieste. Dall'altra parte non sarebbe possibile processare le richieste con un solo thread in quanto il centisimo utente aspetterebbe il completamento delle 99 richieste precedenti prima di vedere arrivare una risposta. "Scalando" (applicando su scala server J) il ragionamento che abbiamo appena fatto per un client sarebbe molto intelligente sfruttare i tempi morti di ogni richiesta per iniziare a processare una seconda richiesta: ad esempio quando viene recuperata una pagina html o una immagine da disco, durante il tempo di attesa necessario per spostare le testine sul disco e rintracciare la risorsa potremmo dare un po' di tempo CPU ad un'altra richiesta: così facendo, come abbiamo esasperato nel primo esempio del web service che impiega due secondi (e sperando che il nostro disco non ci metta due secondi per recuperare una risorsa :-)) una seconda richiesta può essere portata avanti mentre la prima attende una risorsa. E' importante però che il thread che aspetta l'IO del disco venga recuperato e assegnato ad altre operazioni per evitare appunto la creazione (e il costo di gestione) di troppi thread sul sistema. Inoltre, anche la creazione di un thread ha un costo, così come la sua terminazione: sarebbe bene non creare e uccidere un thread per ogni richiesta altrimenti si richia che il costo di creazione/distruzione appesantisca il sistema.

Da questa idea, sin dalle prime versioni di IIS, così come per molti application server, si è pensato alla creazione di un thread pool e a una coda di richieste: mi spiego meglio. Un thread pool è un insieme finito di thread che viene creato in un processo. Partiamo da un caso con pochissime richieste per inquadrare il tutto: nel momento in cui arriva la prima richiesta viene creato un thread T1, nel momento in cui arriva la seconda richiesta viene creato un secondo thread T2; nel momento in cui arriva la terza richiesta è probabile che la prima abbia già terminato l suo lavoro: il thread T1, al termine del lavoro sulla prima richiesta, anzichè essere "ucciso" viene inserito nel pool di thread e reso quindi disponibile alla richiesta numero 3. Arriva la richiesta numero 4: se ci sono thread disponibili nel thread pool la richiesta viene assegnata ad un thread esistente, altrimenti viene creato un nuovo thread T3. Tutto questo fino al limite dei thread configurabile per il thread pool.

Pensate che in IIS 4.0 il numero dei thread del thread pool era impostato per default a 10 !!! In pratica IIS 4 non può processare che 10 richieste contemporanee: prima di spaventarsi ripercorriamo un attimo il ragionamento: occorre sfruttare al massimo i tempi morti durante l'esecuzione di una richiesta/risposta http, come ad esempio l'accesso al disco o a una risorsa in rete (non stiamo ancora parlando di applicazioni ASP.NET) per evitare che il numero di thread cresca a dismisura e causi problemi di performance. Visto che le richieste in IIS prevedono nella maggioranza dei casi l'accesso a risorse su disco (pagine html, immagini e così via) e questo accesso sia solitamente velocissimo (anche per la cache dei file più acceduti) avere un numero di thread così basso consente di mantenere "leggero" il sistema pur fornendo un ottimo throughput. Il rapporto, sempre parlando in media, è circa 100:10 (notare che non è 10:1); questo rapporto indica che per 100 richieste che arrivano via Http sono necessari una decina di thread per soddisfare comunque tutte e cento le richieste per risorse normali; per normali intendo pagine di contenuti, immagini, css; se il server deve fare strema video o scaricare immagini da 100 MB cadauna ovviamente il ragionamento 100:10 dovrebbe essere rivisto. Il punto chiave di questi esempi non sono i numeri di per se, ma la relatività della teoria: voglio dire che il ragionamento non cambia se su un sito con risorse pesanti il rapporto debba essere 100:20.

Supponiamo di avere i 10 thread del thread pool impegnati da richieste esistenti: cosa succede alla undicesima richiesta ? Con la teoria esposta fino a adesso dovremmo rispondere: al client arriverà una risposta "Server Too Busy". Ovviamente non è così altrimenti potremmo affermare che IIS non è un gran che come web server J Le richieste, prima di essere processate, vengono inserite in una coda e prelevate dai vari thread disponibili nel thread pool per essere eseguite. Solo al riempimento di tale coda il server risponderà "Server Too Busy". Si utilizza quindi una tecnica (che ha senso utilizzare nelle nostre applicazioni per scenari simili tramite servizi di accodamento custom o tramite MSMQ) molto intelligente: si accodano le richieste, fino ad un certo limite, e si processano in parallelo solo un numero finito di richieste (con i thread del thread pool) per cercare di ottimizzare il più possibile il bilanciamento fra richieste processabili dal sistema e affaticamento del sistema stesso. Ovviamente questi limiti (thread pool e numero di richieste accodabili, così come altri parametri relativi) possono essere adattati al proprio scenario. Non abbiamo lo spazio per una trattazione esaustiva di questi parametri anche perchè dobbiamo arrivare al cuore dell'articolo ovvero ASP.NET e le pagine asincrone: ricordatevi però sempre che aumentare in modo indiscriminato questi parametri, a prima vista molto bassi, non è una buona idea. Ad esempio superare il valore 60-80 per i thread del thread pool non è quasi mai una buona idea. Vi rimando al Resource Kit di IIS 6.0 per una trattazione approfondita di questi parametri e le tecniche di stress test per verificare il valore ottimale.

 

ASP.NET Thread Pool

Se la richiesta che ha raggiunto IIS è destinata ad una risorsa ASP.NET, questa viene inviata al motore di ASP.NET da ASPNET_ISAPI.dll e, a questo punto senza sorpresa, vi comunico che anche ASP.NET ha una coda per gestire le richieste prima di essere assegnate ai thread di un thread pool interno al processo di ASP.NET (o i processi di ASP.NET nel caso di IIS 6).

Anche ASP.NET ragiona quindi con la teoria che abbiamo appena esposto: appena arriva una richiesta, questa viene accodata; viene poi recuperato un thread del thread pool per processare la richiesta. Anche in questo caso il riempimento della coda fa sì che venga rifiutata la richiesta. Per default ASP.NET ha un thread pool di 25 thread (alcuni devono però essere usati dal motore per operazioni di routine come controllo della cache, ricompilazione, gestione delle session...anche se sapete da sempre che non andrebbero usate !!!).

Non dovrebbe sembrare strano, a questo punto, che il valore di default del thread pool di IIS sia più basso rispetto a quello di ASP.NET: la richiesta arriva a IIS e viene accodata, appena si rende disponibile un thread si inizia a processare la richiesta che viene passata a ASP.NET. Il thread in IIS può essere rimesso immediatamente nel pool e reso disponibile per altre richieste (HTML, immagini e così via) mentre ASP.NET processa la richiesta, sicuramente più complessa, rispetto a prelevare un semplice file da disco. Se arriva una seconda richiesta per ASP.NET, magari è lo stesso thread che ha inviato la prima richiesta ad inviare anche la seconda richiesta. Quando le risposte da parte di ASP.NET sono pronte viene recuperato un thread libero nel thread pool di IIS per mandare la risposta al client. Ottimo no ?

 

ASP.NET Sync/Async

A questo punto diventa chiara la seguente affermazione: occorre occupare per meno tempo possibile i thread del thread pool durante l'esecuzione di una richiesta in modo da renderli disponibili alle richieste entranti. Come fare ? Se una elaborazione occupa tempo CPU (ad esempio per fare dei calcoli) difficilmente possiamo fare a meno di occupare un thread, ma se la richiesta deve accedere a risorse (disco, rete, web service, database) possiamo liberare il prima possibile il thread (effettuando una chiamata asincrona) in modo da farlo lavorare su un'altra richiesta. Predisponiamo il terreno anche in ASP.NET per effttuare i vari test che proseguiremo in un prossimo articolo dove andremo in dettaglio su ASP.NET 1.x e ASP.NET 2.0 per capire le differenze e le nuove facilitazioni.

Per testare il tutto ho preparato la seguente Master Page che visualizza (menu per le varie demo a parte) un trace delle operazioni e il sorgente della pagina richiesta. In pratica la master page espone un metodo, richiamabile dalle varie pagine delle demo, AddTraceMessage che visualizza il tempo dall'inizio della richiesta, il numero di thread occupati e liberi del thread pool e la stringa di trace. Questa la mia master page:

 

--- Master.Master ---

<%@  Language="C#" %>

<%@ Import Namespace="System.IO" %>

<%@ Import Namespace="System.Threading" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

 

<script runat="server">

 

    void Page_Init()

    {

        AddTraceMessage("Page_Init");

        Page.PreRenderComplete += new EventHandler(this.PagePreRenderComplete);

    }

 

    void PagePreRenderComplete(Object source, EventArgs e)

    {

        AddTraceMessage("PreRenderComplete");

 

        using (TextReader r = new StreamReader(Request.PhysicalPath))

        {

            SourceLabel.Text = HttpUtility.HtmlEncode(r.ReadToEnd());

        }

    }

 

    StringBuilder _trace = new StringBuilder();

    DateTime _pageStartTime = DateTime.Now;

 

    public void AddTraceMessage(string message)

    {

        double t = (DateTime.Now - _pageStartTime).TotalSeconds;

        Int32 i;

        Int32 totalWorkerThreads;

        // Numero massimo di thread del thread pool

        ThreadPool.GetMaxThreads(out totalWorkerThreads, out i);

        Int32 availableWorkerThreads;

        ThreadPool.GetAvailableThreads(out availableWorkerThreads, out i);

        // Thread di tutto il processo

        Int32 totalThreadCounts = System.Diagnostics.Process.GetCurrentProcess().Threads.Count;

       

 

        lock (_trace)

        {

            _trace.AppendFormat("[{0:000}] {1:00.000} - {5} - [{3}/{4}/{6}] -- {2}\r\n",

                Thread.CurrentThread.GetHashCode(), t, "Thread " + AppDomain.GetCurrentThreadId() + " " + message,

                totalWorkerThreads - availableWorkerThreads, totalWorkerThreads,

                Thread.CurrentThread.IsThreadPoolThread ? "TP" : "!TP",

                totalThreadCounts);

        }

    }

</script>

 

<html xmlns="http://www.w3.org/1999/xhtml">

<head id="Head1" runat="server">

    <title>Sample Page</title>

</head>

<body>

    <form id="form" runat="server">

        <table>

            <tr>

                <td valign="top">

                    <p>

                        <asp:SiteMapPath ID="SiteMapPathNode" runat="server" Font-Names="Verdana" PathSeparator=" : "

                            EnableViewState="False">

                            <PathSeparatorStyle Font-Bold="True" ForeColor="#990000" />

                            <CurrentNodeStyle ForeColor="#333333" />

                            <NodeStyle Font-Bold="True" ForeColor="#FF8000" />

                            <RootNodeStyle Font-Bold="True" ForeColor="#990000" />

                        </asp:SiteMapPath>

                        <asp:SiteMapDataSource ID="SiteMapDS" runat="server" />

                    </p>

                    <p>

                        <asp:TreeView ID="SiteTreeView" runat="server" DataSourceID="SiteMapDS" ImageSet="Simple"

                            Font-Names="Verdana" Font-Size="0.9em" EnableViewState="False" NodeWrap="True">

                            <SelectedNodeStyle ForeColor="#990000" Font-Bold="True" />

                            <NodeStyle VerticalPadding="2px" ForeColor="Black" />

                            <HoverNodeStyle Font-Underline="True" ForeColor="#5555DD" />

                            <ParentNodeStyle />

                        </asp:TreeView>

                    </p>

                </td>

                <td valign="top">

                    <asp:Label ID="Label1" runat="server" Text="Page Trace:" Font-Bold="True" Font-Names="Verdana"

                        Font-Size="0.8em" ForeColor="Maroon" />

                    <pre><%= _trace.ToString() %></pre>

                    <hr />

                    <asp:Label ID="Label2" runat="server" Text="Page Source:" Font-Bold="True" Font-Names="Verdana"

                        Font-Size="0.8em" ForeColor="Maroon" />

                    <pre><asp:Label ID="SourceLabel" runat="server" /></pre>

                </td>

            </tr>

        </table>

    </form>

</body>

</html>

 

 

Come si può notare, viene intercettato il Page_Init per inserire un messaggio iniziale nel trace e abbonarsi all'evento PreRenderComplete nel cui event handler viene letto il sorgente della pagina e valorizzata la label che lo visualizza. Il PreRenderComplete è un punto importante nell'esecuzione di pagine asincrone e lo analizzeremo in dettaglio nel prossimo articolo.

 

A questo punto possiamo costruire la prima pagina (referenziando la master page) che esegue una chiamata sincrona al nostro famoso (e lento) web service che impiega 2 secondi per essere eseguito.

 

--- Sync Page Chiama 1 Web Service  ---

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

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

 

<script runat="server">

 

    void Page_Load() {

        Master.AddTraceMessage("SalvaAgente 1.0 Sync");

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

        bool ris = ws.SalvaAgente("robertob", "RobertoBrunetti");

        Master.AddTraceMessage("SalvaAgente 1.0 Finito");

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

    }

 

</script>

 

Il codice non è diverso da quanto facevamo nel click del bottone della Windows Form all'inizio dell'articolo. Viene istanziato il web service e invocato il metodo SalvaAgente. Il resto del codice ci consente di inserire messaggi nel trace, che come abbiamo visto nella master page, visualizza informazioni importanti sui thread. Ecco il risultato della esecuzione (depurato del sorgente e del menu per questioni di spazio):

 

[007] 00,000 - TP - [1/100/25] -- Thread 2264 Page_Init

[007] 00,000 - TP - [1/100/25] -- Thread 2264 SalvaAgente 1.0 Sync

[007] 02,016 - TP - [1/100/27] -- Thread 2264 SalvaAgente 1.0 Finito

[007] 02,016 - TP - [1/100/27] -- Thread 2264 Risultato True

[007] 02,016 - TP - [1/100/27] -- Thread 2264 PreRenderComplete

 

Il trace ci mostra, in prima battuta, che la pagina ha richiesto 2,016 secondi per essere eseguita: ricordatevi che sotto i due secondi non possono scendere in quanto il metodo del nostro web service impiega almeno due secondi per essere eseguito. Per semplicità ho pubblicato tutti gli esempi all’indirizzo http://thinkmobile.it:9000/async: visto che la master page visualizza il sorgente non avete bisogno di riscriverli…copiateli direttamente.

 

Il trace mostra 5 messaggi di trace, che grazie al metodo esposto dalla master page visualizza, per ogni chiamata, l'hash code del thread, il tempo di esecuzione dall'inizio della richiesta, TP nel caso in cui si stia utilizzando un thread del thread pool, l'id del thread e il messaggio passato come parametro al metodo. Page_Init e PreRenderComplete vengono aggiunti dal codice della master page. I valori fra parentesi quadre [X/XXX/XX] rappresentano rispettivamente il numero dei thread impegnati nel thread pool, il numero massimo di thread ospitabili e il numero di thread liberi a disposizione.

Come si può notare per eseguire questa pagina che effettua la chiamata sincrona al metodo del web service viene impiegato un solo thread (il 2264) e il tempo fra la chiamata (SalvaAgente 1.0 Sync) e la risposta (SalvaAgente 1.0 Finito) è di poco superiore ai due secondi: questo indica, come abbiamo avuto modo di dire più volte, che il thread aspetta l'intero ciclo della chiamata al metodo. In pratica con una chiamata sincrona stiamo impegnando un solo thread del thread pool per tutta la durata della richiesta; se il thread pool dispone di 20 thread significa che potremmo effettuare fino a 20 richieste in contemporanea e che la ventunesima richiesta aspetterebbe in coda circa due secondi per essere iniziata a processare. Se la coda può ospitare al massimo 100 richieste significa che prima di arrivare a processare la 81 richiesta occorre aspettare 4 trance da 2 secondi (8 secondi almeno). Inoltre la coda si libera di 20 posti dopo almeno due secondi: in pratica a regime possiamo accettare massimo 20 richieste ogni 2 secondi (supponendo sempre che le richieste arrivino nello stesso istante).

 

Proviamo a chiamare due web service in modo sincrono:

 

--- Sync Page Chiama 2 Web Service  ---

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

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

 

<script runat="server">

 

    void Page_Load() {

        Master.AddTraceMessage("SalvaAgente 1.0 Sync");

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

        bool ris = ws.SalvaAgente("robertob", "RobertoBrunetti");

        Master.AddTraceMessage("SalvaAgente 1.0 Finito");

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

 

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

        ris = ws2.SalvaAgente("paolopi", "PaoloPialorsi");

        Master.AddTraceMessage("SalvaAgente 1.0 Finito");

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

       

    }

 

</script>

 

Ed ecco il risultato:

 

[001] 00,000 - TP - [1/100/22] -- Thread 3292 Page_Init

[001] 00,000 - TP - [1/100/22] -- Thread 3292 SalvaAgente 1.0 Sync

[001] 02,547 - TP - [1/100/24] -- Thread 3292 SalvaAgente 1.0 Finito

[001] 02,563 - TP - [1/100/24] -- Thread 3292 Risultato 1 True

[001] 04,578 - TP - [1/100/24] -- Thread 3292 SalvaAgente 1.0 Finito

[001] 04,578 - TP - [1/100/24] -- Thread 3292 Risultato 2 True

[001] 04,578 - TP - [1/100/24] -- Thread 3292 PreRenderComplete

 

Anche in questo caso stiamo impegnando un solo thread (il 3292) per l'intera esecuzione della pagina che giustamente dura non meno di 4 secondi (4,578 in questo caso) perchè le due richieste ai due web service sono sequenziali e sincrone e non consentono di parallelizzare le due richieste. Ovviamente abbiamo peggiorato l'esecuzione in quanto adesso ogni richiesta impiega non meno di 4 secondi quindi possiamo accettare un massimo di 20 richieste ogni 4,5 secondi circa.

 

Stress (ma non troppo) Test

 

Mettendo il sito sotto stress rispetto ad un tempo di risposta di 2,2 secondi circa per la chiamata singola, otteniamo un tempo medio di risposta di 16,3 secondi e varie risposte Server Too Busy per il problema accennato: il dramma però sta nel fatto che il processore della macchina è al 24% di utilizzo; in pratica i tempi di risposta diventano inaccettabili anche con pochi client su una macchina che non è carica: il processore passa la maggior parte del tempo ad aspettare le risposte remote e non viene sfruttato come potrebbe. Mettere un processore più potente per cercare di velocizzare l'applicazione sarebbe l'errore più grande che potremmo fare, in quanto il collo di bottiglia non è il processore, ma il tempo di attesa sincrono della nostra banalissima applicazione. Se portate questo esempio nel mondo reale dove una pagina di un portale fa una decina di chiamate remote a web service otteniamo una situazione catastrofica anche con 20 utenti collegati e un hardware da decine di migliaia di euro.

Dobbiamo migliorare i tempi di attesa e quindi sfruttare meglio la CPU. La soluzione però non è effettuare chiamate asincrone di per se. Vediamo il perchè con un esempio che purtroppo viene raccomandato da molti come soluzione al problema appena citato. Prima di implementarlo aspettate di vedere i risultati e leggere il prossimo articolo dove cercherò di spiegare il motivo per cui modificare solamente le chiamate sincrone in asincrone sia più distruttivo. Il motivo che scopriremo è che ASP.NET 1.x esegue le pagine in modo sincrono e questo comportamento è il default anche per ASP.NET 2.0. Andiamo per gradi.

 

Proviamo quindi a iniziare con il pattern BeginMetodo a eseguire la chiamata in asincrono. Ecco il codice:

 

--- Sync Page Chiama 1 Web Service BeginXXX   ---

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

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

 

<script runat="server">

 

    void Page_Load() {

        Master.AddTraceMessage("SalvaAgente 1.0 Begin");

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

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

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

       

        // Occorre aspettare la fine

        ar.AsyncWaitHandle.WaitOne();

    }

   

    private void AgenteSalvato(IAsyncResult ar)

    {

        Master.AddTraceMessage("SalvaAgente 1.0 End");

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

        bool ris = ws.EndSalvaAgente(ar);

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

    }

</script>

 

Prima di vedere i risultati e fare le dovute considerazioni spendiamo un attimo sulla chiamata a ar.AsyncWaitHandle.WaitOne() che non era presente nel primo esempio asincrono su una Windows Form: questa riga serve per evitare che l'esecuzione della pagina finisca (ed è molto probabile per non dire certo in questo semplice esempio) prima che la chiamata asincrona termini. In pratica si imposta un WaitHandle per segnalare l'attesa sulla chiusura della chiamata asincrona: senza questa istruzione la chiamata asincrona verrebbe comunque completata ma non potremmo visualizzare nessun risultato al client in quanto la risposta sarebbe già stata chiusa da ASP.NET.

 

Chiarito il concetto vediamo i risultati:

[007] 00,000 - TP - [1/100/27] -- Thread 2264 Page_Init

[007] 00,000 - TP - [1/100/27] -- Thread 2264 SalvaAgente 1.0 Begin

[007] 00,000 - TP - [1/100/27] -- Thread 2264 SalvaAgente 1.0 Finito

[005] 02,016 - TP - [1/100/33] -- Thread 2692 SalvaAgente 1.0 End

[005] 02,016 - TP - [1/100/33] -- Thread 2692 Risultato True

[007] 02,016 - TP - [1/100/33] -- Thread 2264 PreRenderComplete

 

Non ci discostiamo molto come tempi di esecuzione dall'esempio sincrono ed è normale visto che la chiamata comunque impiega due secondi ma come si nota dal trace entra in gioco un secondo thread (il 2692 rispetto al 2264) per la visualizzazione del risultato. Proviamo con due chiamate asincrone:

 

--- Sync Page Chiama 2 Web Service BeginXXX   ---

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

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

 

<script runat="server">

 

    void Page_Load() {

        Master.AddTraceMessage("SalvaAgente 1.0 Begin");

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

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

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

       

        Master.AddTraceMessage("SalvaAgente 1.0 Begin");

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

        IAsyncResult ar2 = ws2.BeginSalvaAgente("robertob", "RobertoBrunetti", AgenteSalvato, ws);

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

       

        // Occorre in qualche modo aspettare gli Endxxx prima di abbandonare la pagina

        // Tip: Se possibile fare prima la Wait sul comando più veloce (più veloce in Begin e in End)

        //          Così intanto il secondo va avanti

        ar.AsyncWaitHandle.WaitOne();

        ar2.AsyncWaitHandle.WaitOne();

 

    }

   

    private void AgenteSalvato(IAsyncResult ar)

    {

        Master.AddTraceMessage("SalvaAgente 1.0 End");

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

        bool ris = ws.EndSalvaAgente(ar);

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

    }

</script>

 

Ecco il risultato:

 

[007] 00,000 - TP - [1/100/27] -- Thread 2264 Page_Init

[007] 00,016 - TP - [1/100/27] -- Thread 2264 SalvaAgente 1.0 Begin

[007] 00,016 - TP - [1/100/29] -- Thread 2264 SalvaAgente 1.0 Finito

[007] 00,016 - TP - [1/100/29] -- Thread 2264 SalvaAgente 1.0 Begin

[007] 00,016 - TP - [1/100/29] -- Thread 2264 SalvaAgente 1.0 Finito

[017] 02,203 - TP - [2/100/30] -- Thread 2792 SalvaAgente 1.0 End

[017] 02,203 - TP - [2/100/30] -- Thread 2792 Risultato True

[017] 02,703 - TP - [1/100/30] -- Thread 2792 SalvaAgente 1.0 End

[017] 02,703 - TP - [1/100/30] -- Thread 2792 Risultato True

[007] 02,703 - TP - [1/100/30] -- Thread 2264 PreRenderComplete

 

Ecco in gioco un altro thread (notare che i numeri, anche se casualmente, sono gli stessi proprio perchè i thread vengono recuperati dal thread pool) per eseguire la seconda richiesta asincrona e il conseguente risultato.

 

Sembra tutto perfetto: siamo risciti a parallelizzare le richieste abbassando il tempo totale di risposta....ma le insidie sono ben nascoste dietro questo esempio.

 

In questo esempio, infatti, stiamo provando la pagina con un solo browser e analizzando i migliorati tempi di risposta...ma cosa succede se effettuiamo più richieste in contemporanea ?

 

Due dati per capire il problema con un carico di 20 client:

La pagina con la chiamata a 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 occorre 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 due 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 va peggiorare 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 la vedremo nel prossimo articolo (più avanti nel testo) e consta 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.

Nel prossimo articolo vedremo le tecniche all'opera sia per ASP.NET 1.x che per ASP.NET 2.0 analizzando anche i metodi più automatizzati come AddOnPreRenderCompleteAysnc e Page Task.

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

Vi chiedo un favore: visto che il sito thinkmobile.it è una comunità per tutti gli sviluppatori mobile (fra cui gli sviluppatori web mobile in ASP.NET 1.x e 2.0) vi pregherei di non effettuare stress test direttamente sull'indirizzo pubblico ma semplicemente visualizzare live gli esempi e copiare il codice per effettuare stress test privati sui vostri server.

 

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, 07:55 PM by rob | with no comments
Filed under: ,