Introduction
The execution of a request within the aspnet_wp.exe process is done on the same thread from end to end. For this, ASP.NET uses the I/O threads of the CLR’s thread pool, and can execute multiple requests simultaneously. In addition to avoiding a certain number of concurrency problems by design, this approach is generally more efficient as the reception of requests from a name pipe starts directly on an I/O thread. We then avoid the transmission of information between threads. Since there is no named pipes in IIS 6.0, things happen a little differently, and in this version, ASP.NET uses several worker threads of the CLR.
It happens that the processing of a request implies a long wait for the thread. Typically, this happens when we access a slow web service, or when we complete an expensive request on a database. In this case, the ASP.NET threading model is inefficient. In fact, the wait of a thread is not without consequences, since the number of threads in the pool used to serve the request can be limited. If most requests imply a wait from their threads, the server will quickly find itself overloaded. We are then faced with a design bottleneck.
ASP.NET 2.0 offers an infrastructure to solve this problem. The idea is that the finalization of the processing of a request be accomplished on a different thread than the one that initiated the processing. Meanwhile, the access to a resource, such as a web service or a database, is done without holding a thread from the pool. In the case of a SQL Server database access, this is possible using the asynchronous requests of ADO.NET 2.0. This feature allows making a request to a SQL Server database without using a thread from the pool while waiting for the data. The following example illustrates a page which uses the feature, allowing ASP.NET 2.0 to process a page on multiple threads, and the feature of ADO.NET 2.0 allowing asynchronous requests. The data recovered from the database is then presented in a GridView
:
<%@ Page Language="C#" Async="true"
CodeFile="Default.aspx.cs" Inherits="_Default" %>
<html xmlns="http://www.w3.org/1999/xhtml" >
<body>
<form id="form1" runat="server">
<asp:GridView ID="MyGridView" runat="server"
AutoGenerateColumns="true" />
</form>
</body>
</html>
using System;
using System.Data;
using System.Data.SqlClient;
using System.Web;
using System.Web.UI;
using System.Threading;
public partial class _Default : System.Web.UI.Page {
private SqlCommand cmd;
private SqlConnection cnx;
protected void Page_Load(object sender, EventArgs e) {
Response.Write("PageLoad thread:"+
Thread.CurrentThread.ManagedThreadId +
"<br/>");
PageAsyncTask task = new PageAsyncTask( BeginInvoke,
EndInvoke, EndTimeOutInvoke, null);
Page.RegisterAsyncTask(task);
}
public IAsyncResult BeginInvoke( object sender,
EventArgs e,
AsyncCallback callBack,
object extraData) {
Response.Write( "BeginInvoke thread:" +
Thread.CurrentThread.ManagedThreadId +
"<br/>");
cnx = new SqlConnection( "async=true ; server" +
" = localhost ; " + "uid=sa ; pwd =; " +
"database = ORGANIZATION");
cmd = new SqlCommand( "SELECT * FROM EMPLOYEES", cnx );
cnx.Open();
return cmd.BeginExecuteReader( callBack, extraData,
CommandBehavior.CloseConnection);
}
public void EndInvoke( IAsyncResult result ) {
Response.Write( "EndInvoke thread:" +
Thread.CurrentThread.ManagedThreadId + "<br/>");
SqlDataReader rdr = cmd.EndExecuteReader(result);
if (rdr != null && rdr.HasRows) {
MyGridView.DataSource = rdr;
MyGridView.DataBind();
}
rdr.Close();
}
public void EndTimeOutInvoke( IAsyncResult result ) {
if (cnx != null && cnx.State != ConnectionState.Closed) {
cnx.Close();
}
Response.Write("TimeOut");
Response.End();
}
protected override void OnPreRender( EventArgs e ) {
Response.Write( "OnPreRender thread:" +
Thread.CurrentThread.ManagedThreadId +
"<br/>");
}
protected override void OnPreRenderComplete( EventArgs e ) {
Response.Write( "OnPreRenderComplete thread:" +
Thread.CurrentThread.ManagedThreadId +
"<br/>");
}
}
To support this multi-threaded processing model, you must set the Async
property of the <%@ Page>
directive to true
. During the Load
event, we can create instances of the System.Web.UI.PageAsyncTask
class. Each instance represents a processing which will occur outside of the aspnet_wp.exe process. No thread from the pool is used to wait for the end of the processing. This processing is represented by three delegates:
- A delegate to the method that will begin the processing of the request (i.e., the one which executes
Page_Load()
). In our example, the delegate references the BeginInvoke()
. It is executed right after the PreRender()
event.
- A delegate to the method which completes the processing of the work on a thread of the pool once the work is completed. In our example, our delegate references the
BeginInvoke
method. It is executed right before the PreRenderComplete
event.
- A delegate to a method executed by a thread of the pool when the work was not completed within a certain amount of time. In this case, our delegate references the
EndTimeOutInvoke()
method.
The HTML page generated by this example starts as follows:
PageLoad thread:4
OnPreRender thread:4
BeginInvoke thread:4
EndInvoke thread:8
OnPreRenderComplete thread:8
...
If you wish to use this technique to asynchronously process a call to a web service, you can use the IAsyncResult BeginXXX()
and EndXXX(IAsyncResult)
methods of the proxy class generated by the wsdl.exe tool.