Practical ASP.NET

Build More Scalable Sites

Learn how to integrate ASP.NET's built-in security tools with the features of your existing site.

Technology Toolbox: Visual Basic, ASP.NET, XML

Most business applications are data driven.

Your ASP.NET page sits and waits until the data comes back from the remote process whenever your code reads data from a database server or calls a Web Service on another server. This means that most ASP.NET pages in business applications spend a lot of time doing nothing. Your page is successfully performing one task during this time: It is tying up a thread from your application's thread pool. When enough pages do nothing, you exhaust the threads in your server's thread pool, and new visitors to your site must wait until a thread becomes available. At best, this results in longer response times; at worst, users start getting "Server too busy" errors and are denied access to the site.

I measured the effects of tying up a thread by creating a test client that issued 50 simultaneous requests to an ASP.NET page that, in turn, called a Web Service. The average response time for those 50 requests was more than eight seconds as new requests waited for threads tied up by old requests to become available.

The solution is to implement asynchronous processing. Doing so means your page's thread is returned to the thread pool, so another page can use it while your own code waits for results. When the remote server returns its results, a new thread picks up from where your page left off. The average response time for a new set of 50 calls by the client fell to less than two seconds when I switched my test page to asynchronous processing.

Some .NET functions support asynchronous processing out of the box. For example, the proxy class that .NET generates for a Web Service includes Begin and End versions of every method in the service that support asynchronous processing. Unfortunately, the Web environment makes asynchronous processing difficult. By the time that the remote server returns its results, your page may have finished executing, rendered the HTML to be sent to the user, and sent the page on its way to the client. In other words, it might be too late to affect the contents of a given page by the time the results are returned. Using asynchronous processing effectively means that you need some way to hold your page in memory—without, of course, tying up a thread.

Fortunately, taking advantage of asynchronous processing in ASP.NET makes it easy to exploit the asynchronous features built into the .NET Framework, while ensuring that your page waits for the remote server to return. Note that the response time in my test case improved, but asynchronous processing isn't a tool for speeding up your application in all situations. The improvement in response time is a side effect of throwing threads back into the pool when a client had nothing else to do. My response time improved because I had other clients waiting for a thread. Asynchronous processing actually increases the demand on your server's CPU because additional cycles must be spent starting and stopping threads. You should implement asynchronous processing only if you are in danger of running out of threads because asynchronous processing adds a small amount of overhead to your application and can degrade performance marginally.

Wire Up Asynchronous Processing
Using asynchronous page processing requires that you first enable asynchronous processing, then add the code that wires up your application logic to the page's asynchronous processing framework. Simply set the Async attribute in the Page directive to true in Source view to enable asynchronous processing for a page:

<%@ Page Async="true" ... 

You can begin wiring your application code into the asynchronous processing by defining two routines that hold your code: one routine to call the long running process (the "Begin routine") and one routine to catch and process the results (the "End routine"). The sample I describe in this article calls a Web Service in the Begin routine (CallWebService) and catches the results of the Web Service in the End routine (ProcessWebServiceResults). You must declare these routines with specific signatures to integrate with ASP.NET's asynchronous processing:

Function CallWebService(ByVal sender As Object, _
ByVal e As System.EventArgs, _
ByVal cb As System.AsyncCallback, _
ByVal extraData As Object) As System.IAsyncResult
End Function
Sub ProcessWebServiceResults(ByVal ar As System.IAsyncResult)
End Sub

ASP.NET's asynchronous processing depends on calling BeginEventHandler and EndEventHandler delegates, which means you must tie your routines into these delegates. Begin by declaring these two delegates and tying them to your existing routines:

Dim Begin As BeginEventHandler
Dim End As EndEventHandler

Begin = New BeginEventHandler(AddressOf CallWebService)
End = New EndEventHandler(AddressOf ProcessWebServiceResults)

You need to do one more thing to set up asynchronous processing: Tie the two delegates into your page's asynchronous processing framework. Do this by calling the Page object's AddOnPreRenderCompleteAsync method, passing it the Begin and End delegates:

Me.AddOnPreRenderCompleteAsync(Begin, End)

Once you set up asynchronous processing by calling AddOnPreRenderCompleteAsync, ASP.NET calls your Begin routine automatically after the Page object's PreRender event finishes. Page processing then halts until the remote process returns its results. At this point, ASP.NET calls your End routine. The Page object's PreRenderComplete event executes after your End routine finishes executing, rendering the Page object's HTML and sending the results to the client. You need to wire up these routines early in the life of the page—no later than the Page's Load event. ASP.NET calls your routines automatically, as part of the Page's lifecycle, so you should only wire up your code when you want the remote server called.

This example wires up the routines only if the page is being posted back. Note that the example takes advantage of anonymous methods to reduce the five previous statements to a single call, AddOnPreRenderAsync (see Figure 1):

If Me.IsPostBack = True Then
Me.AddOnPreRenderCompleteAsync( _
New BeginEventHandler(AddressOf _
CallWebService), _
New EndEventHandler(AddressOf  
ProcessWebServiceResults))
End If

Add Application Code
Your framework is in place, so you can add your application code to your Begin and End routines. You have two responsibilities to fulfill in these routines. First, your Begin routine must return an AsyncResult object. Second, your End routine code is passed an object with the IAsyncResult interface; use this to retrieve the results of your asynchronous call. You're now ready to put any other code you need in these routines.

You might be curious why the sample relies on accessing Web Services and databases. I chose these because they are typical activities in business applications, and .NET supports asynchronous processing in both of these activities. For example, the Begin* version of any Web Service method returns an AsyncResult object, while the End* version of the method accepts an IAsyncResult object that ties it to the corresponding Begin* method. This makes integrating a call to a Web Service from the Begin and End methods of a Page object easy. The same is true of using the SqlCommand object's BeginExecute* methods.

This sample calls the BeginSquare version of a method called Square on the Web Service and retrieves the results from the EndSquare version of the same method in the routines that you wired into the Page object's asynchronous processing previously:

Function CallWebService(ByVal sender As Object, _
ByVal e As System.EventArgs, _
ByVal cb As System.AsyncCallback, _
ByVal extraData As Object) As System.IAsyncResult
Dim Number As Integer
Number = Convert.ToInt32(Me.DataTextBox.Text)
Return ws.BeginSquare(Number, cb, extraData)

End Function
Sub ProcessWebServiceResults(ByVal ar As _
System.IAsyncResult)
Dim Result As String
Result = ws.EndSquare(ar).ToString
sMe.ResultTextBox.Text = Result
End Sub

Your application gives up all threads from the ASP.NET application-thread pool while the Web Service processes the request. The only cost to your server is from the BeginSquare method. This method is an I/O request and draws a thread from the I/O thread pool (the Worker thread pool in Windows 2003), a more forgiving source than the application-thread pool that your page uses.

Using AddOnPreRenderCompleteAsync is convenient, but it isn't a perfect solution for asynchronous processing in complicated applications. There are three major limitations related to using AddOnPreRenderCompleteAsync. First, calling more than one asynchronous process with AddOnPreRenderCompleteAsync requires you to write a custom IAsyncResult object. Second and much more critically, a remote service that doesn't get back to you for a long time will cause your page to wait for an equally long time. Finally, the HttpContext.Current object used by the thread at the start of your application isn't available at the end of your application, so impersonation and culture are not transferred to End routine. You can solve all of these problems by using RegisterAsyncTask at the cost of sinking a little more time into setting up your asynchronous processing (see Figure 2).

Using RegisterAsyncTask requires that you first create a PageAsyncTask object. The steps for doing this are similar to the steps for creating the AddonPreRenderCompleteAsync object, except that you pass three parameters. You must pass a BeginEventHandler and an EndEventHandler as you did in the earlier example. But you must also pass a second EndEventHandler. Point the second EndEventHandler to a routine to call if your remote server takes too long to return its results. (You must declare this routine with the same signature that you used for your End routine.) Once you create a PageAsyncTask object, pass it to the Page object's RegisterAsyncTask method to have your routines called after the PreRender event. You can repeat this process for as many asynchronous calls as you want to make from your page.

You can pass additional data you want to make available to your Begin routine as a fourth parameter when you create the PageAsyncTask object. ASP.NET passes this data to the Begin routine automatically in the extraData parameter. Passing data through the extraData parameter ensures that your Begin events don't contend over a shared resource when they execute.

This code defines two PageAsyncTasks (CallSquare and CallAdd) and registers them:

Dim CallAdd As New PageAsyncTask( _
New BeginEventHandler( _
AddressOf CallAddService), _
New EndEventHandler(AddressOf  _  
ProcessAddResults), _
New EndEventHandler(AddressOf _
OopsSquareHandler), _
SecondNumber)
Me.RegisterAsyncTask(CallAdd)
Dim CallSquare As New PageAsyncTask( _
New BeginEventHandler(AddressOf _
CallSquareService), _
New EndEventHandler(AddressOf _
ProcessSquareResults), _
New EndEventHandler(AddressOf _
OopsAddHandler), Nothing)
Me.RegisterAsyncTask(CallSquare)

Note that the current value of the variable SecondNumber is passed as the fourth parameter when creating CallAdd object.

This code illustrates how the CallAddService extracts the number passed in the code:

Function CallAddService(ByVal sender As Object, _
ByVal e As System.EventArgs, _
ByVal cb As System.AsyncCallback, _
ByVal extraData As Object) As System.IAsyncResult
Dim OtherNumber As Integer
OtherNumber = Convert.ToInt32(extraData)

You must still set the Async attribute in the Page directive. You can also specify the timeout period for your page in the Page directive or set it through the Page object's AsycTimeout property. For example, this code shows turns on Async processing and sets the timeout to five seconds:

<%@ Page Async="true"  AsyncTimeout="5"

You can set the time out period though the AsyncTimeout property each time that your page is called, but the timeout value applies to the entire page; you can't set a separate timeout value for each request.

Calling multiple routines asynchronously can create contention problems when the multiple End routines execute as results are returned. In my original AddOnPreRenderCompleteAsync code, I updated a text box on my page from the End routine. I had only one asynchronous process, so there was no danger of contention. However, when you have multiple End routines, you should update the private variables that are used exclusively by each End routine to avoid contention issues when results are returned. You can then process these results in a fixed order in the PreRenderComplete event, which doesn't execute until all of your asynchronous processes have finished (see Listing 1).

That wraps up this article's sample. As is often the case, what's shown here only hints at how you might use the approach described. For example, the SqlCommand object includes BeginExecuteReader, BeginExecuteNonQuery, and BeginExecuteXMLReader methods (with supporting End methods) that you can use in the same way as the Begin* and End* methods of the WebService routines described in this article. If you want to call objects through remoting that execute on an application server, the asynchronous method calls built into .NET delegates let you call those remote objects using asynchronous page processing. Note that you must define your own delegates to these remote objects.

Regardless, the asynchronous tools that ASP.NET provides help you ensure that when your pages won't tie up limited application thread-pool resources when they have nothing to do.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube