Techniques for Issuing Asynchronous
|
protected void Page_Load(object sender, EventArgs e)
{
// Instantiate Web service proxy for retrieving data.
//
using (PortalServices ps = new PortalServices())
{
// Invoke Web service asynchronously
// and harvest results in callback.
//
ps.GetNewsHeadlinesCompleted +=
new GetNewsHeadlinesCompletedEventHandler(
ps_GetNewsHeadlinesCompleted);
ps.GetNewsHeadlinesAsync();
}
}
// This callback is invoked when the async Web service completes.
//
void ps_GetNewsHeadlinesCompleted(object sender,
GetNewsHeadlinesCompletedEventArgs e)
{
// Extract results and bind to BulletedList.
//
_newsHeadlines.DataSource = e.Result;
_newsHeadlines.DataBind();
}
|
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="AsynchronousPage.aspx.cs"
Inherits="AsynchronousPage"
Async="true"
%>
|
Once we update the other two Web parts that use Web services to retrieve their data asynchronously like this one, our page is much more
[1] Note that if you are calling multiple Web service methods asynchronously, you must do so through separate instances of the Web proxy class because of the way it is built internally.
Before we look at how to make our database access asynchronous, it's worth spending a little time on exactly how the asynchronous Web method calls work. This model of asynchronous method invocation is new to .NET 2.0, and in general is simpler to work with than the standard Asynchronous Programming Model (APM) involving the IAsyncResult interface. Note that we didn't even have to touch an IAsyncResult interface, nor did we have to let the containing page know that we were performing asynchronous operations (by registering a task or some other technique), and yet it all seemed to work as we had hoped.
The secret lies in the Web service proxy class' implementation of the asynchronous method, along with a helper class introduced in .NET 2.0 called the AsyncOperationManager. When we called the GetNewsHeadlinesAsync method of our proxy class, it mapped the call onto an internal helper method of the SoapHttpClientProtocol base class from which the proxy class derives, called InvokeAsync. InvokeAsync does two important things: it registers the asynchronous operation with the AsyncOperationManager class by calling its static CreateOperation method, and it then launches the request asynchronously using the WebRequest class' BeginGetRequestStream method. At this point the call returns and the page goes on processing its lifecycle, but because the page has been
[2] While the request thread will be returned to the thread pool in this example, keep in mind that it is still used to process the ADO.NET query in its entirety before being returned to the pool. Because of this, we are not really
relieving pressure on the thread pool because the thread is not returned to the pool quickly enough to make it available to service other requests while our page is still awaiting I/O-bound tasks to complete.
Once the asynchronous Web request completes, it will invoke the method we subscribed to the completed event of the proxy on a separate thread drawn from the I/O thread pool. If this is the last of the asynchronous operations to complete (kept track of by the synchronization context of the AsyncOperationManager), the page will be called back and the request will complete its processing from where it left off, starting at the PreRenderComplete event. Figure 9-4 shows this entire lifecycle when you use asynchronous Web requests in the context of an asynchronous page.
The AsyncOperationManager is a class that is designed to be used in different environments to help in the management of asynchronous method invocations. For example, if you called a Web service asynchronously from a WinForms application, it would also tie into the AsyncOperationManager class. The difference between each environment is something called the SynchronizationContext associated with the AsyncOperationManager. When you are running in the context of an ASP.NET application, the SynchronizationContext will be set to an instance of the AspNetSynchronizationContext class, whose primary purpose is to keep track of how many outstanding asynchronous requests are pending so that when they are all complete, the page request processing can resume.
Let's now get back to our problem of making our data access asynchronous, and the general issue of performing asynchronous data retrieval with ADO.NET. There is
IAsyncResult BeginExecuteReader(AsyncCallback ac, object state) IAsyncResult BeginExecuteNonQuery(AsyncCallback ac, object state) IAsyncResult BeginExecuteXmlReader(AsyncCallback ac, object state)
and the corresponding completion methods once the data stream is ready to begin reading:
SqlDataReader EndExecuteReader(IAsyncResult ar) int EndExecuteNonQuery(IAsyncResult ar) XmlReader EndExecuteXmlReader(IAsyncResult ar)
To use any of these asynchronous retrieval methods, you must first add "async=true" to your connection string. For our scenario, we are interested in populating a GridView by binding it to a SqlDataReader, so we will use the BeginExecuteReader method to initiate the asynchronous call.
To tie this into our asynchronous page, ASP.NET 2.0 also supports the concept of registering asynchronous tasks you would like to have executed prior to the page completing its rendering. This is a more explicit model than the one we used with our Web service proxies, but it also gives us some more flexibility because of that. To register an asynchronous task, you create an instance of the PageAsyncTask class and initialize it with three delegatesa begin handler, an end handler, and a timeout handler. Your begin handler must return an IAsyncResult interface, so this is where we will launch our asynchronous data request using BeginExecuteReader. The end handler is called once the task is complete (when there is data ready to read in our example), at which point you can use the results. ASP.NET will take care of invoking the begin handler just before it relinquishes the request thread (immediately after the PreRender event completes). Listing 9-5 shows the updated implementation of our sales report data retrieval, which
public partial class AsyncPage : Page
{
// Local variables to store connection and command for async
// data retrieval
//
SqlConnection _conn;
SqlCommand _cmd;
protected void Page_Load(object sender, EventArgs e)
{
// ... Web service calls not shown ...
string dsn = ConfigurationManager.
ConnectionStrings["salesDsn"].ConnectionString;
string sql = "WAITFOR DELAY '00:00:03' SELECT [id], [quarter], " +
"[year], [amount], [projected] FROM [sales] WHERE year=@year";
// Append async attribute to connection string
//
dsn += ";async=true";
_conn = new SqlConnection(dsn);
_cmd = new SqlCommand(sql, _conn);
_conn.Open();
_cmd.Parameters.AddWithValue("@year", int.Parse(_yearTextBox.Text));
// Launch data request asynchronously using
// page async task
//
PageAsyncTask salesDataTask = new PageAsyncTask(
new BeginEventHandler(BeginGetSalesData),
new EndEventHandler(EndGetSalesData),
new EndEventHandler(GetSalesDataTimeout),
null, true);
Page.RegisterAsyncTask(salesDataTask);
}
IAsyncResult BeginGetSalesData(object src, EventArgs e,
AsyncCallback cb, object state)
{
return _cmd.BeginExecuteReader(cb, state);
}
void EndGetSalesData(IAsyncResult ar)
{
try
{
_salesGrid.DataSource = _cmd.EndExecuteReader(ar);
_salesGrid.DataBind();
}
finally
{
_conn.Close();
}
}
void GetSalesDataTimeout(IAsyncResult ar)
{
// Operation timed out, so just clean up by
// closing connection
if (_conn.State == ConnectionState.Open)
_conn.Close();
_messageLabel.Text = "Query timed out...";
}
}
|
Note that we could use this same technique with our Web service requests by using the alternate asynchronous methods provided on the proxy class (BeginGetNewsHeadlines, for example). One potential advantage to this technique over the simpler
The asynchronous task feature of ASP.NET also supports the concept of task ordering and setting up dependencies between tasks. For example, imagine that you had a page that had five I/O-bound tasks to perform, but that task 1 had to be completed before tasks 2, 3, and 4 could be started (most likely because they took data as input that was retrieved in task 1), and that task 5 could not be started until all of the other four tasks had completed. Figure 9-5 shows the dependence chain of our five tasks.
To set up dependencies like this, the constructor of the PageAsyncTask class has a final Boolean parameter that indicates whether the task is to be performed in parallel or not. If it is set to false, the task will be executed to completion before the next registered task is started. To achieve the set of dependencies outlined in Figure 9-5, we would mark tasks 1 and 5 as false for parallel execution, and tasks 2, 3, and 4 as true. By then registering the tasks with the page (by calling RegisterAsyncTask) in order from 1 to 5, we would achieve the desired ordering and dependency enforcement, as shown in Listing 9-6.
protected void Page_Load(object sender, EventArgs e)
{
PageAsyncTask task1 = new PageAsyncTask(
new BeginEventHandler(BeginTask1),
new EndEventHandler(EndTask1),
new EndEventHandler(Task1Timeout),
null, false);
PageAsyncTask task2 = new PageAsyncTask(
new BeginEventHandler(BeginTask2),
new EndEventHandler(EndTask2),
new EndEventHandler(Task2Timeout),
null, true);
PageAsyncTask task3 = new PageAsyncTask(
new BeginEventHandler(BeginTask3),
new EndEventHandler(EndTask3),
new EndEventHandler(Task3Timeout),
null, true);
PageAsyncTask task4 = new PageAsyncTask(
new BeginEventHandler(BeingTask4),
new EndEventHandler(EndTask4),
new EndEventHandler(Task4Timeout),
null, true);
PageAsyncTask task5 = new PageAsyncTask(
new BeginEventHandler(BeginTask5),
new EndEventHandler(EndTask5),
new EndEventHandler(Task5Timeout),
null, false);
RegisterAsyncTask(task1);
RegisterAsyncTask(task2);
RegisterAsyncTask(task3);
RegisterAsyncTask(task4);
RegisterAsyncTask(task5);
}
// Individual begin/end task methods not shown
|
The combination of parallel execution and the ability to specify dependencies makes asynchronous tasks extremely flexible. You have the ability to define parallel tasks using the standard IAsyncResult interface, specify timeouts, set up dependencies between tasks, and execute these tasks in either a synchronous or an asynchronous page. There is a significant amount of threading and synchronization code backing up this feature, which takes the