Asynchronous ASP.NET Page Processing

Asynchronous programming offers certain benefits in creating high-availability applications. The asynchronous model provides the ability to switch execution of the "the DoWork" method to another thread. This can be important for client applications in order to avoid blocking the UI. It also provides the ability to execute several tasks on different threads at the same time. Finally, the async model gives us the ability to make calls to network resources in a way that does not block any threads.


For the .NET Framework, the asynchronous API pattern was first introduced in v1.0. It involves the Begin/End methods found in many .NET components such as the System.IO and System.Net APIs, delegates (BeginInvoke/EndInvoke), WebService proxies, and in .NET 2.0, SqlClient. 

When the BeginXXX method is used, the Result Method(params) becomes:
IAsyncResult BeginMethod(params, callback, state)Result EndMethod(IAsyncResult)

The .NET 2.0 Framework and above offer us a new Event-based Asynchronous programming model that makes asynchronous programming more friendly to the average developer (no  IAsyncResult / AsyncCallback). It is Implemented by some .NET components including Web Service proxies. 

In .NET 2.0 and higher, our Result Method(params) now becomes:
void MethodAsync(params)event MethodCompletedMethodCompletedEventHandlerMethodCompleted

EventArgs has our Result property It should be mentioned that ASP.NET v1.x does support asynchronous APIs via IHttpAsyncHandler. This allows asynchronous calls, but it can’t use page framework features or asynchronous processing of HTTP pipeline events. 
It is possible to call a web service asynchronously either from GLOBAL.ASAX or an HTTP Module.

The ASP.NET 2.0 Asynchronous Page model 
The previously-mentioned asynchronous API model is available in .NET 2.0, and the advantages are that more components implement this Framework pattern than the newer event-based pattern. It offers the lower level API for developers who prefer to implement the IAsyncResult / IAsyncCallback pattern. 

The disadvantages are that the Framework pattern is the least developer-friendly. It doesn’t flow through impersonation, culture, or the HttpContext to the "end handler", and it is difficult to combine multiple IAsyncResult objects.

The ASP.NET 2.0 Event-based Asynchronous model looks like this:
On Page_Load - subscribe to the completion event and call the asynchronous method:

ws.MethodCompleted += new MethodCompletedEventHandler(target method);
ws.MethodAsync(params);

On the completion event handler - retrieve the result:

void MyMethodCompletedHandler(object source, MethodCompletedEventArgs e) 
{ resultVar = e.Result;}

The advantages of using this model are that it is easy to launch several parallel async operations. You also get automatic flow of impersonation, culture, and HttpContext.Current to the completion event handler. So for example, if you needed to make 4 different WebRequests to various resources at the same time in order to combine them after completion in the UI of the page for display, this is a situation where the pattern could be useful. 

Be aware however, that this is not some magic bullet for "behind the scenes" multithreaded processing, because Page class lifecycle completion is haltedpending the return of all async method calls. What this does do, however, is give you more flexibility to do processing of work in parallel. 

In GLOBAL.ASAX or HTTP modules, each application pipeline event has the async equivalent to support the Framework async pattern. An event-based async operation can be launched from any application event.  The next event will be executed when all async operations complete. 

In ASP.NET pages, pages must be marked as async="true". It is possible to launch async work using different async patterns even on the same page. The async patterns can be used in ASP.NET controls, including custom controls and DataSources, when the controls are on async pages. 

With just one attribute, by decorating the @Page directive with Async='true', developers can spawn one or more long-running operations, returning the main thread to ASP.NET to be used for other requests. This defers the completion of page processing until the asynchronous operations have completed and the page has the information it needs to complete the request.

The options to do this include calling the AddOnPreRenderCompleteAsync or RegisterAsyncTask methods of your page instance, calling an XXXAsync method of a component that supports the event-based asynchronous pattern, or any number of other asynchronous execution techniques. In this sample page app, I will focus on the RegisterAsyncTask method, which is probably the most developer-friendly and intuitive. We will make two separate WebRequests for RSS feeds, executing both requests in parallel, loading each into a DataSet. We will also provide a Timeout EndEventHandler to handle and report timeout conditions. 

Let's take a look at some page code first, and then I will walk through the process with a short explanation, and some additional comments at the end:

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Text;
using System.Net;
using System.IO;
 
public partial class samples_MultiWebRequest : System.Web.UI.Page
{
HttpWebRequest req1;
HttpWebRequest req2;
IAsyncResult result1;
IAsyncResult result2;
DataSet ds1 = new DataSet();
DataSet ds2 = new DataSet(); 
 
protected void Page_Load(object sender, EventArgs e)

// Register the two separate WebRequests as AsyncTasks,
// specifying that they should be run in parallel:
Page.RegisterAsyncTask( new PageAsyncTask(new BeginEventHandler(this.BeginAsyncWork1),
new EndEventHandler(this.EndAsyncWork1), new EndEventHandler(this.TimeoutHandler), true) );
Page.RegisterAsyncTask(new PageAsyncTask(new BeginEventHandler(this.BeginAsyncWork2),
new EndEventHandler(this.EndAsyncWork2), new EndEventHandler(this.TimeoutHandler), true));
}
 
protected override void OnPreRenderComplete(EventArgs e)
{
base.OnPreRenderComplete(e);
// write the result messages to the Label.
MessageOut();
}
 
IAsyncResult BeginAsyncWork1(Object sender, EventArgs e, AsyncCallback cb, object state)
{
AddTraceMessage("BeginAsyncWork"); 
AddTraceMessage("BeginGetRSSOne");
this.req1 = (HttpWebRequest)WebRequest.Create("http://www.eggheadcafe.com/forumrss.aspx?topicid=2");
req1.Method = "GET";
 req1.Proxy = System.Net.GlobalProxySelection.GetEmptyWebProxy(); 
result1= req1.BeginGetResponse(cb,req1); 
return result1;
}
 
IAsyncResult BeginAsyncWork2(Object sender, EventArgs e, AsyncCallback cb, object state)
{
AddTraceMessage("BeginGetRSSTwo");
this.req2 = (HttpWebRequest)WebRequest.Create("http://www.eggheadcafe.com/forumrss.aspx?topicid=4");
req2.Method = "GET";
req2.Proxy = System.Net.GlobalProxySelection.GetEmptyWebProxy();
result2 = req2.BeginGetResponse(cb, req2);
return result2;
}
 
void EndAsyncWork1(IAsyncResult asyncResult)

AddTraceMessage("EndAsyncWork1"); 
if (result1!=null)
{
AddTraceMessage("EndGetRssOne"); 
WebResponse response = (WebResponse)req1.EndGetResponse(asyncResult);
Stream streamResponse = response.GetResponseStream();
ds1 = new DataSet();
ds1.ReadXml(streamResponse);
streamResponse.Close();
}
 
}
void EndAsyncWork2(IAsyncResult asyncResult)
{
AddTraceMessage("EndAsyncWork2"); 
if (result2 != null)
{
AddTraceMessage("EndGetRssTwo");
WebResponse response2 = (WebResponse)req2.EndGetResponse(asyncResult);
Stream streamResponse2 = response2.GetResponseStream();
ds2 = new DataSet();
ds2.ReadXml(streamResponse2);
streamResponse2.Close();
}
}
 
void TimeoutHandler(IAsyncResult asyncResult)
{
AddTraceMessage("Request Timed Out");
Response.Write("<strong>async Request timed out</strong><br />");
}
 
private void MessageOut()
{
Page.Trace.Write(_trace.ToString());
if (ds1.Tables.Count > 0)
this.Label1.Text += "Got " + ds1.Tables[2].Rows.Count.ToString() + " Rows from response1 <br>";
 
if (ds2.Tables.Count > 0)
this.Label1.Text += "Got " + ds2.Tables[2].Rows.Count.ToString() + " Rows from response2 <br>";
}
 
StringBuilder _trace = new StringBuilder();
DateTime _pageStartTime = DateTime.Now;
public void AddTraceMessage(string message)
{
double t = (DateTime.Now - _pageStartTime).TotalSeconds;
lock (_trace)
{
_trace.AppendFormat("Thread:[{0:000}] {1:00.000} -- {2}\r\n",
System.Threading.Thread.CurrentThread.GetHashCode(), t, message);
}
}
}

In the beginning, at class level, I've defined my two WebRequests, my two IAsyncResult objects, and two DataSets, one each to hold an RSS feed that is returned. 

In my Page_Load handler, I register my two Tasks - each with the last parameter set to "true" to ensure parallel operations. So in Page_Load, both task are automatically kicked off, with further page processing halted until they both return or one or more are timed out.

As each EndAyncWork callback is executed, I grab my WebResponse out of the AsyncResult object, create a stream, and have the respective DataSet perform its ReadXml method from the contents of the stream, loading the RSS Xml into a DataSet.

If a request Times out , which timeout is controlled via the <@Page AsyncTimeout="2" directive at the top of the ASPX portion of my page, the Timeout callback is fired by the AsyncTask framework, and I would get a timeout message for that request. At this point I could, for example, combine the two resultsets into a new DataTable and use it to display the combined RSS feeds in a GridView if I liked.  You can see the async action in the Trace output, which I have turned on in the demo page:

aspx.pageBegin PreRenderComplete0.5651477062048660.554446Thread:[005] 00.277 -- BeginAsyncWork
Thread:[005] 00.277 -- BeginGetRSSOne
Thread:[012] 00.531 -- EndAsyncWork1
Thread:[012] 00.531 -- EndGetRssOne
Thread:[012] 00.786 -- BeginGetRSSTwo
Thread:[013] 00.807 -- EndAsyncWork2
Thread:[013] 00.807 -- EndGetRssTwo
0.5662519940975590.001104 aspx.pageEnd PreRenderComplete 0.5669990382576360.000747

There are several points to be aware of about this process:
The AsyncTimeout @Page directive specifies the total time budget for the Page, not a "per-task" or "per-async operation" timeout.  Provided the timeout doesn't expire, and all parallel tasks complete, Page execution then continues and the page is rendered to the browser. The BeginXXX Event handler is always invoked. It is important to understand that Asynchronous Page processing kicks off one or more long-running operations (here, "Tasks"), and then returns the thread to ASP.NET to be used for other requests. Completion of page processing is deferred until the long-running operations have completed (or the Page times out, if specified) and the page has the information it needs to complete the request.

Regarding the BeginXXX event handler always being invoked, essentially what this means is that if you have Async Tasks to kick off in your page, and the Page Timeout has already been invoked, they are basically doomed. This means that the intent of the timeout property is really to allow the developer to know that the Page has already exceeded the timeout and to know not to kick off any further async tasks. So, it's important not only to provide a Timeout handler, but also to consider having it set a flag that's visible to the Async Task methods so they can decide whether they should or should not go into the Black Hole of Asynchronous Processing (BHAP, if you are an acronym freak). 

No comments:

Post a Comment

Flipkart