Articoli DevLeap

Articoli DevLeap

January 2006 - Posts

ASP.NET 2.0 Async Tecniques

Quando si parla di Asynchronous Programming ci si riferisce all’esecuzione di operazioni su altri thread. Questa tecnica, consolidata da anni, è utile su un’applicazione Windows o mobile per evitare il blocco dell’interfaccia utente durante operazioni potenzialmente lunghe. Già con il .Net Framework versione 1.x molte classi implementavano il pattern BeginMetodo, EndMetodo per eseguire il Metodo in modalità asincrona. Per esempio, anche sul .Net Compact Framework 1.0 è possibile invocare un Web service che espone il metodo ListSalesmen usando BeginListSalesmen e EndListSalemen. Questi metodi sono generati in automatico alla creazione della classe proxy del Web service.
Nel .Net Framework 2.0 troviamo un nuovo pattern, che usa la sintassi MetodoAsync per eseguire la chiamata asincrona e l’evento MetodoCompleted che rappresenta la callback al termine dell’esecuzione di Metodo.

Questo semplifica la scrittura del codice non richiedendo la definizione o l’uso di IAsyncResult e AsyncCall­back. Anche Ado.Net 2.0 espone metodi Begin/End per l’esecuzione di comandi sul database: è possibile inviare una query in modo asincrono senza sospendere il thread corrente in attesa della risposta da parte del database server, ricevendo una chiamata a una funzione specificata (callback) al termine delle operazioni.
Queste tecniche lato client evitano di bloccare il thread che gestisce l’interfaccia utente e si possono parallelizzare più richieste a risorse remote, diminuendo il tempo totale di attesa: se, per esempio, un processo deve invocare due Web service remoti e/o eseguire due query su un database server, possiamo parallelizzare le quattro operazioni diminuendo il tempo totale che, con chiamate sincrone, risulterebbe essere almeno la somma dei tempi delle quattro chiamate.

L’uso di queste tecniche server-side richiede invece qualche spiegazione aggiuntiva. Se una pagina Asp.Net deve prelevare i dati da tre Web service remoti, eseguire la richiesta in modo asincrono evita, come nell’esempio precedente, di dover attendere la fine della prima richiesta per effettuare la seconda, migliorando il tempo totale d’esecuzione.
Eseguendo la pagina dal browser si notano immediatamente i benefici: i problemi in Asp.Net 1.x si presentano non appena proviamo a usare la pagina da più browser in contemporanea.
Vediamo il perché. Tutti gli application server, per evitare la creazione di un thread per soddisfare ogni singola richiesta (arrivando a generare migliaia di thread se aumenta il numero di richieste contemporanee), utilizzano un thread pool, ovvero un insieme finito di thread da cui prelevare i vari thread per eseguire le richieste: Asp.Net (1.x e 2.0) hanno un thread pool di default costituito da 20 thread per processo e per processore (configurabile con molta cautela nel machine.config).

In Iis 5 tutte le applicazioni Asp.Net girano in un unico processo (ASPNET_WP), mentre con Iis 6 è possibile definire diversi processi, ciascuno dei quali può ospitare diverse applicazioni Asp.Net: questo significa che all’interno di un processo Asp.Net non si possono eseguire più di venti richieste in contemporanea.
Se una pagina dell’applicazione impiega 5 secondi per essere eseguita, occupa un thread del thread pool per 5 secondi: facile arrivare alla conclusione che, se ci sono 20 richieste in contemporanea per la pagina, tutte le altre richieste saranno accodate per almeno 5 secondi.
Se i 5 secondi sono dovuti alla chiamata verso due Web service remoti, sicuramente eseguire le richieste in modo asincrono migliora la velocità d’esecuzione della singola pagina liberando in un tempo minore il thread (che ritorna nel pool disponibile per le richieste accodate).

Il problema su Asp.Net 1.x, è che le richieste effettuate con Begin/End usano un thread dello stesso thread pool: eseguire due richieste asincrone per due Web service peggiora la situazione, in quanto vengono impiegati due thread del thread pool per eseguire le richieste asincrone e un thread che attenda le risposte. La morale è semplice: per ogni richiesta si impiegano 3 thread, quindi, anche se il tempo d’esecuzione totale della singola pagina è inferiore, a fronte di 6 richieste contemporanee, occupiamo 18 thread.
In Asp.Net 1.x è possibile ovviare a questo problema implementando da un HttpHandler custom l’interfaccia IHttpAsyncHandler (le pagine asp.net implementano IHttpHandler dalla classe base System.Web.UI.Page) che consente di liberare il thread che esegue la richiesta per la pagina non appena eseguiamo la chiamata a BeginMetodo. Il thread ritorna nel thread pool e diventa quindi disponibile all’esecuzione di altre richieste durante l’attesa della risposta dal Web service.
Il problema nell’implementare un handler asincrono sta nel fatto che perdiamo la possibilità di utilizzare Web Control, eventi, User Control: la response da inviare al client deve essere prodotta da codice.

Alcuni trucchi per ottenere il beneficio di IHttpAsyncHandler senza rinunciare alle pagine aspx verranno mostrati durante la conferenza e saranno pubblicati sui blog del sito DevLeap dopo la conferenza. In Asp.Net 2.0 le cose si facilitano notevolmente: è sufficiente utilizzare l’attributo Async=true nelle direttive della pagina per fare in modo che la classe autogenerata dietro le quinte implementi IHttpAsyncHandler.
In questo modo l’esecuzione di codice che chiama BeginMetodo o MetodoAsync rilascia il thread corrente, che torna così a essere disponibile al thread pool per soddisfare altre richieste. Un concetto importante è il cosiddetto Async Point che scatta prima del PreRenderComplete nella catena degli eventi di ogni pagina; tutte le richieste asincrone (BeginMetodo o MetodoAsync) devono essere lanciate prima dell’Async Point; se sull’Async Point ci sono ancora operazioni asincrone aperte, l’esecuzione della pagina viene sospesa e il thread corrente ritorna nel thread pool.

Quando tutte le operazioni asincrone si completano, l’esecuzione riprende l’esecuzione della pagina con un thread del thread pool (che può tranquillamente essere diverso dal thread che ha iniziato la richiesta).
Utilizzando pagine asincrone (che implementano IHttpAsyncHandler), le richieste verso risorse esterne come I/O, WebService, Ado.Net nella versione 2.0 utilizzano thread (IOThread) del thread pool di IO che è separato dal thread pool che gestisce le richieste per le pagine (worker thread pool).
I thread in questo pool effettuano operazioni «alertable»: dal punto di vista del sistema operativo, il thread che attende l’esito di un’operazione di I/O può essere utilizzato per fare altre operazioni.

La tecnica usata prende il nome di Apc (asynchronous procedure call) e sebbene richieda l’uso di funzioni sospensive specifiche (tutte le operazioni di I/O e di attesa di eventi da parte dell’utente, per esempio) consente di ottimizzare l’uso dello stesso thread minimizzando al massimo il numero di context switch. Nello scenario di Asp.Net, tale funzionalità risulta utile anche per evitare il proliferare di thread (in un thread pool o meno).
Le richieste in attesa non determinano una sospensione assoluta del thread, ma più una sospensione «logica», che consente la rientranza di codice di altre parti dell’applicativo sfruttando i tempi di attesa «naturali» del thread. Con Asp.Net 2.0 è possibile usare questa tecnica ottimizzata senza doversi preoccupare dei dettagli implementativi (soprattutto di Apc), visto che essi sono risolti dalle classi del Framework.

Ultima nota: Asp.Net 2.0 espone anche una classe chiamata PageTask che consente di gestire task diversi (operazioni da fare) su una pagina per tutti i casi in cui l’errore di esecuzione di una di esse non sia vitale per la pagina stessa: si pensi a un portale la cui home page presenti diverse aree informative; la mancata disponibilità del servizio delle previsioni del tempo non deve bloccare o invalidare tutta la pagina, bensì presentare all’utente la pagina completa senza l’area delle previsioni del tempo. Con la gestione dei timeout su ogni task diventa molto semplice gestire queste casistiche senza dover impazzire con la sincronizzazione dei thread. Le page task sono ammesse anche su pagine sincrone: tutte le operazioni (task) saranno eseguite in modo sincrono con le problematiche accennate in questo articolo e di conseguenza più facilmente debuggabili.

Un esempio di codice http://blogs.devleap.com/rob/archive/2005/11/05/6159.aspx (UPDATED: LINK)

Roberto Brunetti
http://blogs.devleap.com/rob

Posted: Jan 12 2006, 06:06 PM by rob | with no comments
Filed under: ,