Overview
Do you have an ASP.NET page that performs a long-running (>100ms) I/O request, like making an
HTTP request or executing a costly SQL command? Has your ASP.NET web application unexpectedly
hit its maximum throughput? Do ASP.NET web requests start queuing when your web server is under
significant load? If so this article may help you address these issues.
You of course want your web application to handle as many requests as possible. Theoretically you
want the web server to scale perfectly with any increase in the volume of traffic. Of course this
is not possible for many reasons. One of these reasons, the one we will be concerned with here, is
that there is a pool of threads that are allocated for use by incoming ASP.NET requests. Under heavy
load the web server could exhaust this pool which causes request queuing. Unless the heavy load is
very brief this is very bad, more and more requests will queue exhausting the resources of the
web server.
So, what do you do when your web server throughput appears to be maxed out? The easy solution
is to buy another web server for the web farm. The other way is
to reduce the execution time of the page so that the web request thread is released back into
the pool as quickly as possible. Buying a server is fine if you have the money, and optimizing code
is a great idea, but what if the page needs to do a costly I/O request and this request has already
been optimized as much as possible?
To highlight why an I/O request inside a web request is a bad idea let's review this concept of the
thread pool. Suppose the thread pool for ASP.NET requests contains 25 threads. Suppose that a web page
contains an I/O request that takes 2 seconds to complete. In this scenario the maximum throughput for
the page is 12.5 pages per second. If the web server receives 13 or more requests per second incoming
requests will start to queue since all the threads in the pool will be busy.
The trick here is that for the most part the web request threads won't actually be busy. They will
simply be blocking on the I/O call, waiting for the call to return. Wouldn't it be great if
there was a way to release threads that are blocking on I/O calls back to the pool and when the request
returns to allocate a different thread from the pool to complete the web request after the I/O request
is finished? The good news is that in .NET, you can!
Asynchronous Calls to the Rescue
The first thing to note about most I/O bound calls in .NET is that there is typically a way to invoke
them asynchronously. For instance the
System.Data.SqlClient.SqlCommand
object provides a method called
ExecuteNonQuery
that synchronously runs a database
query. Also provided by this class are two methods that are used to do the same thing asynchronously
BeginExecuteNonQuery
and
EndExecuteNonQuery.
Similarly, the
System.Net.HttpWebRequest
class provides a
GetResponse
method for sychronous operations and
BeginGetResponse
and
EndGetResponse
methods for asynchronous operations.
The key to the asynchronous "Begin" methods is that they return an instance of
System.IAsyncResult.
This class is used by callback
handlers to process the result when the asynchronous request has finished. If we were to use the
asynchronous methods to perform the I/O during our web request we could release the web request
thread back to the pool and increase our application's throughput. But before your go using these
methods directly, ASP.NET (2.0+) provides handy ways to hook in these asychronous calls without having to
write all the plumbing yourself.
In fact, ASP.NET provides multiple ways to use asychronous calls to prevent thread blocking.
These include:
Each class serves a specific purpose but the class that is easiest to use and provides flexibility when dealing with
multiple asynchronous requests is System.Web.UI.PageAsyncTask.
PageAsyncTask
The System.Web.UI.PageAsyncTask makes asynchronous calls easy. There are really only
two steps to using this class. The first is to instantiate an instance of it and the second is register the instance
with the System.Web.UI.Page (i.e. the codebehind class of the ASPX page). For example:
Page_Load()
{
// create the asynchronous task instance
PageAsyncTask asyncTask = new PageAsyncTask(
new BeginEventHandler(this.beginAsyncWebRequest),
new EndEventHandler(this.endAsyncWebRequest),
new EndEventHandler(this.asyncWebRequestTimeout),
null);
// register the asynchronous task instance with the page
this.RegisterAsyncTask(asyncTask);
}
The first three parameters of the constructor are delegates to methods that you must write begin and end the
your asyncronous call. In the case of making an HTTP web request they might look like the following:
IAsyncResult beginAsyncWebRequest(
object sender, EventArgs e, AsyncCallback callback, object state)
{
HttpWebRequest request =
(HttpWebRequest)WebRequest.Create("http://www.[something].com/");
IAsyncResult result =
(IAsyncResult)request.BeginGetResponse(callback, request);
return result;
}
void endAsyncWebRequest(IAsyncResult result)
{
HttpWebRequest request = (HttpWebRequest)result.AsyncState;
HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);
...
}
void asyncWebRequestTimeout(IAsyncResult result)
{
HttpWebRequest request = (HttpWebRequest)result.AsyncState;
request.Abort();
}
Note: Most examples of the timeout callback function for a WebRequest task
call the request's EndGetResponse method. This usually works fine but when the web
server is under heavy load the call to EndGetResponse may hang for a few seconds, or more. This can
cause performance issues and in my experience calling the Abort method ensures
a quick return and proper clean-up.
Controlling PageAsyncTask
In order for PageAsyncTask to work at you should include the string "Async=True" in your ASPX's page's @Page directive.
You can also control the timeout for the asychronous task with the "AsyncTimeout=x" parameter where the value for "x"
should be in seconds. This timeout can also be controlled in code via the Page class' AsyncTimeout property.
If you want to set the timeout for all asynchronous tasks in your web application you
can put the following in your web.config file:
<system.web>
<pages asyncTimeout="x" />
</system.web>
Normally, the "begin" method that was specified in the PageAsyncTask's constructor is called between the Page's
PreRender
and
PreRenderComplete
events. In fact the PreRenderComplete was introduced into ASP.NET 2.0 just for the purpose
of doing work just after asynchronous events have completed. However you can request that asynchronous tasks happen
at any time by calling the Page's
ExecuteRegisteredAsyncTasks
method.
The PageAsyncTask class has boolean property called
ExecuteInParallel.
To understand what this is for it's important to note that it is quite possible to register multiple
asynchronous tasks for a single page. For example:
Page_Load()
{
// create and register an asynchronous task for the web request
PageAsyncTask asyncTask = new PageAsyncTask(
new BeginEventHandler(this.beginAsyncWebRequest, ...))
this.RegisterAsyncTask(asyncTask);
// create and register an asynchronous task for the database logging request
asyncTask = new PageAsyncTask(
new BeginEventHandler(this.beginLoggingRequest, ...))
this.RegisterAsyncTask(asyncTask);
}
When multiple tasks are registered the ExecuteInParallel property
can be used to determine if they are executed sequentially (in the order they were registered) or
in parallel. It is very important to note the AsyncTimeout is for the
the sum total of all asynchronous requests, so if ExecuteInParallel is set to
"true" and two asychronous tasks are registered where each task could take up to 2 seconds, then
AsyncTimeout should be set to at least 4 seconds.