Asynchronous Pages in ASP.NET 2.0

Asynchronous Pages in ASP.NET 2.0

ASP.NET 2.0 is replete with new features ranging from declarative data binding and Master Pages to membership and role management services. But my vote for the coolest new feature goes to asynchronous pages, and here's why.
When ASP.NET receives a request for a page, it grabs a thread from a thread pool and assigns that request to the thread. A normal, or synchronous, page holds onto the thread for the duration of the request, preventing the thread from being used to process other requests. If a synchronous request becomes I/O bound—for example, if it calls out to a remote Web service or queries a remote database and waits for the call to come back—then the thread assigned to the request is stuck doing nothing until the call returns. That impedes scalability because the thread pool has a finite number of threads available. If all request-processing threads are blocked waiting for I/O operations to complete, additional requests get queued up waiting for threads to be free. At best, throughput decreases because requests wait longer to be processed. At worst, the queue fills up and ASP.NET fails subsequent requests with 503 "Server Unavailable" errors.
Asynchronous pages offer a neat solution to the problems caused by I/O-bound requests. Page processing begins on a thread-pool thread, but that thread is returned to the thread pool once an asynchronous I/O operation begins in response to a signal from ASP.NET. When the operation completes, ASP.NET grabs another thread from the thread pool and finishes processing the request. Scalability increases because thread-pool threads are used more efficiently. Threads that would otherwise be stuck waiting for I/O to complete can now be used to service other requests. The direct beneficiaries are requests that don't perform lengthy I/O operations and can therefore get in and out of the pipeline quickly. Long waits to get into the pipeline have a disproportionately negative impact on the performance of such requests.
The ASP.NET 2.0 Beta 2 async page infrastructure suffers from scant documentation. Let's fix that by surveying the landscape of async pages. Keep in mind that this column was developed with beta releases of ASP.NET 2.0 and the .NET Framework 2.0.

Asynchronous Pages in ASP.NET 1.x
ASP.NET 1.x doesn't support asynchronous pages per se, but it's possible to build them with a pinch of tenacity and a dash of ingenuity. For an excellent overview, see Fritz Onion's article entitled "Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code" in the June 2003 issue of MSDN®Magazine.
The trick here is to implement IHttpAsyncHandler in a page's codebehind class, prompting ASP.NET to process requests not by calling the page's IHttpHandler.ProcessRequest method, but by calling IHttpAsyncHandler.BeginProcessRequest instead. Your BeginProcessRequest implementation can then launch another thread. That thread calls base.ProcessRequest, causing the page to undergo its normal request-processing lifecycle (complete with events such as Load and Render) but on a non-threadpool thread. Meanwhile, BeginProcessRequest returns immediately after launching the new thread, allowing the thread that's executing BeginProcessRequest to return to the thread pool.
That's the basic idea, but the devil's in the details. Among other things, you need to implement IAsyncResult and return it from BeginProcessRequest. That typically means creating a ManualResetEvent object and signaling it when ProcessRequest returns in the background thread. In addition, you have to provide the thread that calls base.ProcessRequest. Unfortunately, most of the conventional techniques for moving work to background threads, including Thread.Start, ThreadPool.QueueUserWorkItem, and asynchronous delegates, are counterproductive in ASP.NET applications because they either steal threads from the thread pool or risk unconstrained thread growth. A proper asynchronous page implementation uses a custom thread pool, and custom thread pool classes are not trivial to write (for more information, see the .NET Matters column in the February 2005 issue of MSDN Magazine).
The bottom line is that building async pages in ASP.NET 1.x isn't impossible, but it is tedious. And after doing it once or twice, you can't help but think that there has to be a better way. Today there is—ASP.NET 2.0.
 

Asynchronous Pages in ASP.NET 2.0
ASP.NET 2.0 vastly simplifies the way you build asynchronous pages. You begin by including an Async="true" attribute in the page's @ Page directive, like so:
<%@ Page Async="true" ... %>
Under the hood, this tells ASP.NET to implement IHttpAsyncHandler in the page. Next, you call the new Page.AddOnPreRenderCompleteAsync method early in the page's lifetime (for example, in Page_Load) to register a Begin method and an End method, as shown in the following code:
AddOnPreRenderCompleteAsync (
    new BeginEventHandler(MyBeginMethod),
    new EndEventHandler (MyEndMethod)
);
What happens next is the interesting part. The page undergoes its normal processing lifecycle until shortly after the PreRender event fires. Then ASP.NET calls the Begin method that you registered using AddOnPreRenderCompleteAsync. The job of the Begin method is to launch an asynchronous operation such as a database query or Web service call and return immediately. At that point, the thread assigned to the request goes back to the thread pool. Furthermore, the Begin method returns an IAsyncResult that lets ASP.NET determine when the asynchronous operation has completed, at which point ASP.NET extracts a thread from the thread pool and calls your End method. After End returns, ASP.NET executes the remaining portion of the page's lifecycle, which includes the rendering phase. Between the time Begin returns and End gets called, the request-processing thread is free to service other requests, and until End is called, rendering is delayed. And because version 2.0 of the .NET Framework offers a variety of ways to perform asynchronous operations, you frequently don't even have to implement IAsyncResult. Instead, the Framework implements it for you.
The codebehind class in Figure 1 provides an example. The corresponding page contains a Label control whose ID is "Output". The page uses the System.Net.HttpWebRequest class to fetch the contents of http://msdn.microsoft.com. Then it parses the returned HTML and writes out to the Label control a list of all the HREF targets it finds.
Since an HTTP request can take a long time to return, AsyncPage.aspx.cs performs its processing asynchronously. It registers Begin and End methods in Page_Load, and in the Begin method, it calls HttpWebRequest.BeginGetResponse to launch an asynchronous HTTP request. BeginAsyncOperation returns to ASP.NET the IAsyncResult returned by BeginGetResponse, resulting in ASP.NET calling EndAsyncOperation when the HTTP request completes. EndAsyncOperation, in turn, parses the content and writes the results to the Label control, after which rendering occurs and an HTTP response goes back to the browser.
 
Figure 2 Synchronous vs. Asynchronous Page Processing
Figure 2 illustrates the difference between a synchronous page and an asynchronous page in ASP.NET 2.0. When a synchronous page is requested, ASP.NET assigns the request a thread from the thread pool and executes the page on that thread. If the request pauses to perform an I/O operation, the thread is tied up until the operation completes and the page lifecycle can be completed. An asychronous page, by contrast, executes as normal through the PreRender event. Then the Begin method that's registered using AddOnPreRenderCompleteAsync is called, after which the request-processing thread goes back to the thread pool. Begin launches an asynchronous I/O operation, and when the operation completes, ASP.NET grabs another thread from the thread pool and calls the End method and executes the remainder of the page's lifecycle on that thread.
 
Figure 3 Trace Output Shows Async Page's Async Point
The call to Begin marks the page's "async point." The trace in Figure 3 shows exactly where the async point occurs. If called, AddOnPreRenderCompleteAsync must be called before the async point—that is, no later than the page's PreRender event.
 

Asynchronous Data Binding
It's not all that common for ASP.NET pages to use HttpWebRequest directly to request other pages, but it is common for them to query databases and data bind the results. So how would you use asynchronous pages to perform asynchronous data binding? The codebehind class in Figure 4 shows one way to go about it.
AsyncDataBind.aspx.cs uses the same AddOnPreRenderCompleteAsync pattern that AsyncPage.aspx.cs uses. But rather than call HttpWebRequest.BeginGetResponse, its BeginAsyncOperation method calls SqlCommand.BeginExecuteReader (new in ADO.NET 2.0), to perform an asynchronous database query. When the call completes, EndAsyncOperation calls SqlCommand.EndExecuteReader to get a SqlDataReader, which it then stores in a private field. In an event handler for the PreRenderComplete event, which fires after the asynchronous operation completes but before the page is rendered, it then binds the SqlDataReader to the Output GridView control. On the outside, the page looks like a normal (synchronous) page that uses a GridView to render the results of a database query. But on the inside, this page is much more scalable because it doesn't tie up a thread-pool thread waiting for the query to return.
 

Calling Web Services Asynchronously
Another I/O-related task commonly performed by ASP.NET Web pages is callouts to Web services. Since Web service calls can take a long time to return, pages that execute them are ideal candidates for asynchronous processing.
Figure 5 shows one way to build an asynchronous page that calls out to a Web service. It uses the same AddOnPreRenderCompleteAsync mechanism featured in Figure 1 and Figure 4. The page's Begin method launches an asynchronous Web service call by calling the Web service proxy's asynchronous Begin method. The page's End method caches in a private field a reference to the DataSet returned by the Web method, and the PreRenderComplete handler binds the DataSet to a GridView. For reference, the Web method targeted by the call is shown in the following code:
[WebMethod]
public DataSet GetTitles ()
{
    string connect = WebConfigurationManager.ConnectionStrings
        ["PubsConnectionString"].ConnectionString;
    SqlDataAdapter adapter = new SqlDataAdapter
        ("SELECT title_id, title, price FROM titles", connect);
    DataSet ds = new DataSet();
    adapter.Fill(ds);
    return ds;
}
That's one way to do it, but it's not the only way. The .NET Framework 2.0 Web service proxies support two mechanisms for placing asynchronous calls to Web services. One is the per-method Begin and End methods featured in .NET Framework 1.x. and 2.0 Web service proxies. The other is the new MethodAsync methods and MethodCompleted events found only in the Web service proxies of the .NET Framework 2.0.
If a Web service has a method named Foo, then in addition to having methods named Foo, BeginFoo, and EndFoo, a .NET Framework version 2.0 Web service proxy includes a method named FooAsync and an event named FooCompleted. You can call Foo asynchronously by registering a handler for FooCompleted events and calling FooAsync, like this:
proxy.FooCompleted += new FooCompletedEventHandler (OnFooCompleted);
proxy.FooAsync (...);
...
void OnFooCompleted (Object source, FooCompletedEventArgs e)
{
    // Called when Foo completes
}
When the asynchronous call begun by FooAsync completes, a FooCompleted event fires, causing your FooCompleted event handler to be called. Both the delegate wrapping the event handler (FooCompletedEventHandler) and the second parameter passed to it (FooCompletedEventArgs) are generated along with the Web service proxy. You can access Foo's return value through FooCompletedEventArgs.Result.
Figure 6 presents a codebehind class that calls a Web service's GetTitles method asynchronously using the MethodAsync pattern. Functionally, this page is identical to the one in Figure 5. Internally, it's quite different. AsyncWSInvoke2.aspx includes an @ Page Async="true" directive, just like AsyncWSInvoke1.aspx. But AsyncWSInvoke2.aspx.cs doesn't call AddOnPreRenderCompleteAsync; it registers a handler for GetTitlesCompleted events and calls GetTitlesAsync on the Web service proxy. ASP.NET still delays rendering the page until GetTitlesAsync completes. Under the hood, it uses an instance of System.Threading.SynchronizationContext, another new class in 2.0, to receive notifications when the asynchronous call begins and when it completes.
There are two advantages to using MethodAsync rather than AddOnPreRenderCompleteAsync to implement asynchronous pages. First, MethodAsync flows impersonation, culture, and HttpContext.Current to the MethodCompleted event handler. AddOnPreRenderCompleteAsync does not. Second, if the page makes multiple asynchronous calls and must delay rendering until all the calls have been completed, using AddOnPreRenderCompleteAsync requires you to compose an IAsyncResult that remains unsignaled until all the calls have completed. With MethodAsync, no such hijinks are necessary; you simply place the calls, as many of them as you like, and the ASP.NET engine delays the rendering phase until the final call returns.
 

Asynchronous Tasks
MethodAsync is a convenient way to make multiple asynchronous Web service calls from an asynchronous page and delay the rendering phase until all the calls complete. But what if you want to perform several asynchronous I/O operations in an asynchronous page and those operations don't involve Web services? Does that mean you're back to composing an IAsyncResult that you can return to ASP.NET to let it know when the last call has completed? Fortunately, no.
In ASP.NET 2.0, the System.Web.UI.Page class introduces another method to facilitate asynchronous operations: RegisterAsyncTask. RegisterAsyncTask has four advantages over AddOnPreRenderCompleteAsync. First, in addition to Begin and End methods, RegisterAsyncTask lets you register a timeout method that's called if an asynchronous operation takes too long to complete. You can set the timeout declaratively by including an AsyncTimeout attribute in the page's @ Page directive. AsyncTimeout="5" sets the timeout to 5 seconds. The second advantage is that you can call RegisterAsyncTask several times in one request to register several async operations. As with MethodAsync, ASP.NET delays rendering the page until all the operations have completed. Third, you can use RegisterAsyncTask's fourth parameter to pass state to your Begin methods. Finally, RegisterAsyncTask flows impersonation, culture, and HttpContext.Current to the End and Timeout methods. As mentioned earlier in this discussion, the same is not true of an End method registered with AddOnPreRenderCompleteAsync.
In other respects, an asynchronous page that relies on RegisterAsyncTask is similar to one that relies on AddOnPreRenderCompleteAsync. It still requires an Async="true" attribute in the @ Page directive (or the programmatic equivalent, which is to set the page's AsyncMode property to true), and it still executes as normal through the PreRender event, at which time the Begin methods registered using RegisterAsyncTask are called and further request processing is put on hold until the last operation completes.To demonstrate, the codebehind class in Figure 7 is functionally equivalent to the one in Figure 1, but it uses RegisterTaskAsync instead of AddOnPreRenderCompleteAsync. Note the timeout handler named TimeoutAsyncOperation, which is called if HttpWebRequest.BeginGetRequest takes too long to complete. The corresponding .aspx file includes an AsyncTimeout attribute that sets the timeout interval to 5 seconds. Also note the null passed in RegisterAsyncTask's fourth parameter, which could have been used to pass data to the Begin method.
The primary advantage of RegisterAsyncTask is that it allows asynchronous pages to fire off multiple asynchronous calls and delay rendering until all the calls have completed. It works perfectly well for one asynchronous call, too, and it offers a timeout option that AddOnPreRenderCompleteAsync doesn't. If you build an asynchronous page that makes just one async call, you can use AddOnPreRenderCompleteAsync or RegisterAsyncTask. But for asynchronous pages that place two or more async calls, RegisterAsyncTask simplifies your life considerably.
Since the timeout value is a per-page rather than per-call setting, you may be wondering whether it's possible to vary the timeout value for individual calls. The short answer is no. You can vary the timeout from one request to the next by programmatically modifying the page's AsyncTimeout property, but you can't assign different timeouts to different calls initiated from the same request.
 

Wrapping It Up
So there you have it—the skinny on asynchronous pages in ASP.NET 2.0. They're significantly easier to implement in this upcoming version of ASP.NET, and the architecture is such that you can batch multiple async I/O operations in one request and delay the rendering of the page until all the operations have completed. Combined with async ADO.NET and other new asynchronous features in the .NET Framework, async ASP.NET pages offer a powerful and convenient solution to the problem of I/O-bound requests that inhibit scalability by saturating the thread pool.
A final point to keep in mind as you build asynchronous pages is that you should not launch asynchronous operations that borrow from the same thread pool that ASP.NET uses. For example, calling ThreadPool.QueueUserWorkItem at a page's asynchronous point is counterproductive because that method draws from the thread pool, resulting in a net gain of zero threads for processing requests. By contrast, calling asynchronous methods built into the Framework, methods such as HttpWebRequest.BeginGetResponse and SqlCommand.BeginExecuteReader, is generally considered to be safe because those methods tend to use completion ports to implement asynchronous behavior.


你可能感兴趣的:(asp.net)