W celu wyjaśnienia zasady działania asynchronicznych stron, najpierw przyjrzyjmy się jak wygląda standardowe zapytanie do serwera. Klient wysyła żądanie HTTP do serwera np. typu GET w celu uzyskania danej strony www. Następnie serwer używa tzw. puli wątków (thread pool). Po prostu przydziela wątek z puli każdemu nadchodzącemu żądaniu. Tworzenie (a raczej odtwarzanie) wątków z puli jest szybkie (o tym już pisałem kiedyś), jednak liczba wątków jest ograniczona. W przypadku gdy serwer będzie musiał obsłużyć zbyt dużą liczbę żądań, zakończy się to błędem “503 – server unavaiable”. Obsługa większości zapytań nie stanowi problemu, jednak gdy aplikacja ASP .NET korzysta z usługi sieciowej lub przetwarza ogromną ilość danych może to stanowić poważne zredukowanie skalowalności.Wyobraźmy sobie, że nasza aplikacja www korzysta z usługi sieciowej. Ponadto połączenie z usługa jest dość wolne – trwa 30 sekund. Gdy przyjdzie zapytanie do serwera, zostanie zdjęty wątek z puli i będzie on zarezerwowany przez ponad 30 sekund (czas połączenia z usługą + inne operacje). Przez ten cały czas wątek nie będzie mógł być wykorzystany do obsługi innych żądań. Co gorsza, wątek większość czasu straci po prostu na oczekiwanie odpowiedzi od usługi sieciowej. Należy podkreślić, że wątki ASP .NET są zbyt cenne aby marnować je na operacje typu IO. Jeśli w systemie rzeczywiście musimy połączyć się z zewnętrznymi zasobami, zróbmy to asynchronicznie!
W przypadku asynchronicznych stron po odebraniu żądania również jest zdejmowany wątek z puli. Jednak zaraz po stwierdzeniu, że mamy do czynienia z asynchroniczną stroną www, wątek jest z powrotem oddawany puli. W tym momencie rozpoczyna się wykonywanie asynchronicznego kodu – czyli np. ładowania danych z usługi sieciowej czy z bazy danych. Po zakończeniu, wątek z powrotem jest zdejmowany z puli i rozpoczyna się już normalne przetwarzanie strony www. Innymi słowy, używamy asynchronicznego przetwarzania dla bardzo czasochłonnych operacji (przeważnie są to operacje typu IO). Na poniższym rysunku przedstawiam standardowy przebieg obsługi żądania w stronach synchronicznych:
Nie ma tu nic nadzwyczajnego. W przypadku asynchronicznych stron, wątek z puli będzie zwolniony w pewnym momencie i sterowanie zostanie przekazane odpowiednim asynchronicznym metodom:
Przejdźmy teraz do implementacji tego w ASP .NET. W wersji 2.0 zostało to znacząco uproszczone. Przede wszystkim należy ustawić atrybut Async na true w dyrektywie @Page:
<%@ Page Async="true" Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>
Ustawienie atrybutu na true tak naprawdę oznacza, że zostanie wykorzystany IHttpAsyncHandler.
Aby rozpocząć część asynchroniczną strony należy wywołać metodę AddOnPreRenderCompleteAsync przekazując odpowiednie handlery:
protected void Page_Load(object sender, EventArgs e)
{
AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation));
}
private IAsyncResult BeginAsyncOperation(object sender, EventArgs e,
AsyncCallback cb, object state)
{
m_Request = WebRequest.Create("http://www.pzielinski.com");
return m_Request.BeginGetResponse(cb, state);
}
private void EndAsyncOperation(IAsyncResult ar)
{
string text;
using (WebResponse response = this.m_Request.EndGetResponse(ar))
{
using (StreamReader reader =
new StreamReader(response.GetResponseStream()))
{
text = reader.ReadToEnd();
}
}
Response.Write(text);
}
W podobny sposób można pobrać dane z bazy danych i przekazać je odpowiedniej kontrolce renderującej (np. ListView).
Ponadto w ASP .NET można definiować tzw. zadania asynchroniczne. Sprawa wygląda podobnie jak w przypadku powyższego kodu. Zadania asynchroniczne wprowadzają dodatkowo kilka rozszerzeń:
-
Oprócz metod BEGIN i END można zdefiniować metodę odpowiedzialną za wykonanie kodu w razie timeout. Wartość timeout można zdefiniować również w dyrektywie @Page za pomocą atrybutu AsyncTimeout.
-
Można rejestrować kilka zadań – dopiero po zakończeniu wszystkich, zostanie wznowione przetwarzanie synchroniczne.
-
Rejestrując zadanie można dodatkowo przekazać jakiś argument jako object (wykorzystywany w metodach BEGIN) .
Przykład:
protected void Page_Load(object sender, EventArgs e)
{
RegisterAsyncTask(new PageAsyncTask(BeginAsyncOperation, EndAsyncOperation, TimeoutOperation, "state"));
}
private IAsyncResult BeginAsyncOperation(object sender, EventArgs e,
AsyncCallback cb, object state)
{
m_Request = WebRequest.Create("http://www.pzielinski.com");
return m_Request.BeginGetResponse(cb, state);
}
private void EndAsyncOperation(IAsyncResult ar)
{
string text;
using (WebResponse response = this.m_Request.EndGetResponse(ar))
{
using (StreamReader reader =
new StreamReader(response.GetResponseStream()))
{
text = reader.ReadToEnd();
}
}
Response.Write(text);
}
private void TimeoutOperation(IAsyncResult ar)
{
}