Techniques for Issuing Asynchronous Tasks


This section introduces the two standard techniques for issuing asynchronous tasks in an ASP.NET page: implicitly using the AsyncOperationManager and explicitly using the PageAsyncTask mechanism.

Asynchronous Web Access

In the sample application described in the previous section, three Web parts are using Web services to retrieve their data, and one is using ADO.NET to access a database. Let's start to introduce parallelism by making the Web service invocations asynchronous, since there is some nice support in the Web service proxy classes generated by WSDL.exe (or the Visual Studio 2005 Add Web Service Reference tool) for performing Web method invocation asynchronously.

When a Web service proxy class is created in ASP.NET 2.0, it actually generates three different ways of invoking any particular methodone synchronous and two asynchronous. For example, the Web service proxy that our Web parts are using has the following methods available for invoking the GetNewsHeadlines Web method:

public string[] GetNewsHeadlines() public IAsyncResult BeginGetNewsHeadlines(                 AsyncCallback callback, object asyncState) public string[] EndGetNewsHeadlines(                 IAsyncResult asyncResult) public void GetNewsHeadlinesAsync() public void GetNewsHeadlinesAsync(object userState) public event GetNewsHeadlinesCompletedEventHandler                  GetNewsHeadlinesCompleted; 


The first method, GetNewsHeadlines, is the standard synchronous method. The next two, BeginGetNewsHeadlines and EndGetNewsHeadlines, can be used to invoke the method asynchronously and can be tied into any number of asynchronous mechanisms in .NET because of the use of the standard IAsyncResult interface. But the most interesting method for our scenario is the last oneGetNewsHeadlinesAsync. To use this method, you must register a delegate with the proxy class' event that was specifically generated to perform asynchronous invocations (the GetNewsHeadlinesCompleted event in our example). The delegate signature is strongly typed to contain the method's return values so that you can easily extract the results in your method implementation. Here is the delegate declaration added to the proxy class for our GetNewsHeadlines method:

public delegate void GetNewsHeadlinesCompletedEventHandler(       object sender, GetNewsHeadlinesCompletedEventArgs e); public partial class GetNewsHeadlinesCompletedEventArgs :                                    AsyncCompletedEventArgs {     private object[] results;     internal GetNewsHeadlinesCompletedEventArgs(             object[] results, Exception exception,             bool cancelled, object userState) :             base(exception, cancelled, userState)     {         this.results = results;     }     public string[] Result     {         get         {             this.RaiseExceptionIfNecessary();             return ((string[])(this.results[0]));         }     } } 


Using this event-based asynchronous method, rewriting our Web method invocation that retrieves the headline news takes two steps. We first subscribe a delegate to the proxy class' GetNewsHeadlinesCompleted event and then call the GetNewsHeadlinesAsync method. In the implementation of the method subscribed to the completed event, we use the resultsin this case by binding them to a BulletedList to display to the client, as shown in Listing 9-3. There's one last step before our Web request will actually launch asynchronously: we must mark the containing page with the Async="true" attribute, as shown in Listing 9-4. This attribute changes several things about the way the page processes requests, which we will explore in detail shortly. For now, just be aware that any time you make asynchronous Web service calls, this attribute must be set to true.

Listing 9-3. Asynchronous Web service call

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(); } 

Listing 9-4. Marking the page as asynchronous

<%@ 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 responsive. In fact, it renders to the client in just over three seconds, just as predicted. Even though our sales report data retrieval is still sequentially accessing the database, our three Web service calls are now invoked asynchronously, so our primary request thread is no longer waiting for their completion.[1] Of course, we ultimately want to have all of our I/O-bound work be asynchronous, so that we can relinquish our primary request thread back to the thread pool as well.

[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.

AsyncOperationManager and Asynchronous Web Service Calls

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 marked with the Async="true" attribute, it will only continue processing the request up to the PreRender event and will then return the request thread to the thread pool.[2]

[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.

Figure 9-4. Asynchronous Web requests in asynchronous pages


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.

Asynchronous Tasks

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 unfortunately no equivalent to the simple asynchronous mechanism exposed by Web service proxies for performing asynchronous data retrieval, so we're going to have to do a little more work to get our final I/O-bound operation to participate in our asynchronous shuffle. What we do have to work with are the new asynchronous methods on the SqlCommand class and the asynchronous task feature of ASP.NET. Using SqlCommand you can now invoke commands asynchronously using one of the following methods:

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 performs asynchronous data access using the asynchronous tasks and the SqlCommand class' asynchronous BeginExecuteReader method.

Listing 9-5. Asynchronous data access using asynchronous tasks

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 delegate-based technique is that you can also specify a timeout handler. If your remote invocations fail to return in time, the associated timeout handler will be invoked. This timeout is specified for the entire page in the @Page directive using the AsyncTimeout attribute, and it defaults to 20 seconds (note that this timeout is for the entire time it takes to process all tasks, not one individually). One other potential advantage of working directly with asynchronous tasks is that they work on pages that are not marked with async="true" in addition to those that are. This gives you the option of performing your tasks in parallel for efficiency without relinquishing the request thread back to the thread pool, an option we will discuss further in the next section.

Dependent Asynchronous Tasks

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.

Figure 9-5. Interdependent asynchronous 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.

Listing 9-6. Asynchronous dependent tasks

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 burden off your shoulders. Most ASP.NET pages that need to introduce asynchronous behavior should end up using asynchronous tasks.




Essential ASP. NET 2.0
Essential ASP.NET 2.0
ISBN: 0321237706
EAN: 2147483647
Year: 2006
Pages: 104

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net