A partire da ASP.NET 2.0, la classe System.Web.UI.Page introduce un metodo non molto conosciuto in grado di facilitare la realizzazione di pagine asincrone: il metodo RegisterAsyncTask. Spesso mi è capitato di vedere implementare chiamate asincrone (es. invocazione di un WebService) in maniera non corretta sfruttando la logica dell' AddOnPreRenderCompleteAsync, quando in realtà molti dei problemi potevano essere gestiti facilmente tramite Task asincroni, che in ASP.NET sono rappresentati dalla classe PageAsyncTask.
Le differenze tra i due approcci non sono moltissime ma significative. Infatti, entrambi richiedono Async="true" nella direttiva @Page (o AsyncMode = true; da codice) e dunque la predisposizione all'esecuzione asincrona della pagina su thread diversi dopo il completamento del PreRender.
Tuttavia, il metodo RegisterAsyncTask introduce i seguenti vantaggi chiave:
- In aggiunta ai metodi Begin/End, permette di registrare un metodo per gestire il timeout di una operazione eventualmente troppo lunga. Il timeout è per-page ed è impostabile tramite l'attributo AsyncTimeout della direttiva @Page: <%@ Page Async="true" AsyncTimeout="10" ... %>
- Permette di gestire più PageAsyncTask (ovvero più "async points") all'interno di un ciclo di vita di una pagina asincrona. Questi a loro volta possono essere eseguiti sia in modalità sequenziale che in modalità parallela (grazie a questo overload del contruttore di PageAsyncTask). Anche in questo caso, analogamente al pattern MetodAsync/MethodCompleted disponibile per le classi proxy dei WS (dal framework 2.0 in poi), ASP.NET "ritarda" il rendering della pagina finché tutti i task asincroni non sono stati completati.
- Permette di passare un oggetto 'state' ai nostri metodi BeginAsync tramite la classe PageAsyncTask.
- Permette di mantenere l'impersonation, la culture e l'oggetto HttpContext.Current nei metodi EndAsync e Timeout (cosa non prevista dal metodo EndAsync registrato con l'approccio AddOnPreRenderCompleteAsync)
Mettiamo insieme il tutto e vediamo un esempio di applicazione dei PageAsyncTask.
Vogliamo elaborare dei feed RSS tramite PageAsyncTask diversi all'interno di una nostra pagina asincrona.
public partial class SampleAsyncPage : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
List<string> feeds = new List<string>();
feeds.Add("http://blogs.msdn.com/MainFeed.aspx?Type=AllBlogs");
feeds.Add("http://blogs.ugidotnet.org/MainFeed.aspx");
feeds.Add("http://channel9.msdn.com/Feeds/RSS/");
foreach (string feed in feeds)
{
RegisterAsyncTask(new PageAsyncTask(new BeginEventHandler(BeginAsyncOperation), // BeginAsync
new EndEventHandler(EndAsyncOperation), // EndAsync
new EndEventHandler(TimeoutAsyncOperation), // TimeoutAsync
WebRequest.Create(feed), // State
true // I task registrati verranno eseguiti in parallelo
)
);
}
}
IAsyncResult BeginAsyncOperation(object sender, EventArgs e, AsyncCallback callback, object state)
{
// In rawFeed abbiamo il feed RSS...
WebRequest webRequest = (WebRequest)state;
return webRequest.BeginGetResponse(callback, state);
}
void EndAsyncOperation(IAsyncResult ar)
{
WebRequest webrequest = (WebRequest)ar.AsyncState;
string rawFeed = null;
using (WebResponse response = webrequest.EndGetResponse(ar))
{
using (StreamReader reader = new StreamReader(response.GetResponseStream())) rawFeed = reader.ReadToEnd();
}
// In rawFeed abbiamo il feed RSS...
}
void TimeoutAsyncOperation(IAsyncResult ar)
{
// Gestione del Timeout per l' AsyncTask
}
}
Per ciascun feed di un nostro elenco registriamo a livello di pagina un PageAsyncTask che in fase di esecuzione si occuperà di reperire lo stream del feed associato (ad esempio grazie alla classe System.Net.WebRequest). Il miglioramento delle prestazioni è garantito anche dal fatto che l'esecuzione asincrona di ciascun Task verrà gestita da ASP.NET possibilmente in parallelo.
Considerazioni:
- Se abbiamo bisogno di una pagina asincrona che gestisca un'unica chiamata asincrona, allora gli approcci AddOnPreRenderCompleteAsync e RegisterAsyncTask sono pressoché equivalenti. Ma se le chiamate asincrone sono molte, è decisamente consigliabile sfruttare le facilitazioni introdotte da RegisterAsyncTask.
- Il timeout è per-page e non per-call. Non possiamo cioè assegnare timeout diversi a chiamate asincrone diverse!!! Al massimo possiamo eventualmente modificare a runtime la proprietà AsyncTimeout, il che ovviamente non è la stessa cosa :D
- In combinazione con async ADO.NET (es. SqlCommand.BeginExecuteReader) ed in generale con le feature asincrone del framework .NET, le pagine ASP.NET asincrone offrono soluzioni molto convenienti per risolvere comuni problemi di I/O legati a scenari che inibiscono la scalabilità.
- In generale non è consigliabile/sicuro gestire operazioni asincrone in ASP.NET utilizzando le API System.Threading (es. ThreadPool.QueueUserWorkItem). Il rischio infatti è quello di "rubare" thread alla normale gestione del ThreadPool di ASP.NET, ottenendo effetti che potrebbero risultare addirittura controproducenti.