April 2010

Volume 25 Number 04

Cutting Edge - Revisiting Asynchronous ASP.NET Pages

By Dino Esposito | April 2010

ASP.NET has always supported synchronous and asynchronous HTTP handlers. Now, ASP.NET 2.0 has new features to make it easier and quicker for developers to create asynchronous pages. Especially for server-based applications, asynchronous operations are fundamental for enabling scalability. If you find you need to scale up your existing Web application, the first aspect to consider is how much asynchrony you can add to the pages.

In this regard, ASP.NET behaves like any other server application that performs some background work on behalf of multiple clients. Each incoming request is assigned to an ASP.NET-owned thread that is picked up from the ASP.NET thread pool. The thread remains blocked until the operation has terminated and some response has been generated for the client. How long should the thread wait? The ASP.NET runtime environment can be configured to define a custom timeout (90 seconds is the default), but it’s more important to prevent the thread from being blocked.

When you deal with potentially lengthy operations, the timeout at most only ensures that after a given number of seconds, the thread will be freed and returned to the pool. Instead, what you want is to keep the thread from being blocked for a long time. Ideally, you want the thread to begin a request and then yield it to some other non-ASP.NET thread. The same thread, or another one from the ASP.NET pool, will be picked up again upon completion of the operation to send the response to the client. This paradigm is known as asynchronous ASP.NET pages.

When it comes to asynchronous operations, you should distinguish between pages that are asynchronous with respect to the user and pages that are asynchronous with respect to the ASP.NET runtime. For pages asynchronous with respect to the user, the only viable approach is an AJAX operation. However, using AJAX to perform a potentially slow operation diminishes the impact on the end user but doesn’t bring any relief to the 
ASP.NET runtime.

Async Pages and the ASP.NET Runtime

The longer the thread hangs onto the request, the longer one thread is subtracted from the ASP.NET pool to serve new incoming requests. When no threads are available to serve new requests, the requests are queued. This may lead to delays and degradation of overall performance.

In ASP.NET, HTTP handlers are synchronous by default. Asynchronous HTTP handlers must be explicitly architected and implemented by applying slightly different interfaces. A synchronous handler differs from an asynchronous handler in one key aspect: instead of the synchronous ProcessRequest method, an asynchronous handler uses the methods listed below, which are part of the IHttpAsyncHandler interface:

IAsyncResult BeginProcessRequest(
     HttpContext context, 
     AsyncCallback cb, 
     object extraData);

void EndProcessRequest(
     
     IAsyncResult result);

BeginProcessRequest contains the operation to be executed to service the request. This code should be designed to start the operation on a secondary thread and return immediately. EndProcessRequest contains the code to complete the request that was previously started.

As you can see, an asynchronous HTTP request is split into two parts—before and after the “async point”—the point in the request lifecycle where the thread that owns the request changes. When the async point is reached, the original ASP.NET thread yields control to another thread. This potentially lengthy operation takes place in between the two parts of the ASP.NET request. Each part of the async request runs independent of the other, with no affinity as far as the thread is concerned. In other words, there’s no guarantee that the same thread will take care of the two parts of the request. The net effect is that no threads are blocked for the duration of the operation.

At this point, the obvious question is: which thread really takes care of the “lengthy” operation? ASP.NET uses I/O completion ports internally to track the termination of a request. When the async point is reached, ASP.NET binds the pending request to an I/O completion port and registers a callback to get a notification when the request has terminated. The OS will use one of its own dedicated threads to monitor the termination of the operation, thus freeing the ASP.NET thread from having to wait in full idle. When the operation terminates, the OS places a message in the completion queue, which triggers the ASP.NET callback that will then pick up one of its own threads to resume the request. As mentioned, I/O completion ports are a feature of the OS.

The Real Nature of Async Pages

In ASP.NET, async pages are commonly associated with the idea of improving the performance of a given page in charge of performing a potentially lengthy operation. However, a few further points should be noted. From the user’s perspective, synchronous and asynchronous requests look nearly the same. If the requested operation is expected to take, say, 30 seconds to complete, the user will wait at least 30 seconds to get the new page back. This happens regardless of the synchronous or asynchronous implementation of the page. Also, don’t be too surprised if an async page ends up taking a bit more time to complete a single request. So what’s the benefit of asynchronous pages?

Scalability is not quite the same as performance. Or, at least, scalability is about performance but on a different level—the whole application instead of a single request. The benefit that async pages bring to the table is much less work for the threads in the ASP.NET pool. This doesn’t make long requests run faster, but it helps the system serve non-lengthy requests as usual—that is, with no special delays resulting from ongoing slow requests.

Async requests take advantage of asynchronous HTTP handlers, which have always been a feature of the ASP.NET platform. However, both ASP.NET Web Forms and ASP.NET MVC provide their own facilities to make it simpler for developers to implement async actions. In the remainder of the article, I’ll discuss asynchronous operations in ASP.NET MVC 2.

Asynchronous Controller Actions

In ASP.NET MVC 1.0, any controller action can only run synchronously. However, a new AsyncController class was added to the MVC Futures library. After an experimental period, the async API for controllers was officially added to the ASP.NET MVC framework, and it’s fully available and documented as of version 2 of the ASP.NET MVC framework. (The syntax and features discussed in this article refer to ASP.NET MVC 2 RC.)  If you play a bit with the AsyncController class in the MVC Futures library, you will notice some changes, and the API is simpler and cleaner.

The purpose of the AsyncController is to ensure that any exposed action methods execute asynchronously without changing the overall approach to programming that characterizes the ASP.NET MVC framework. The diagram in Figure 1 shows the sequence of steps behind the processing of an async action.

Figure 1 Mechanics of an Async Action Method in ASP.NET MVC
Figure 1 Mechanics of an Async Action Method in ASP.NET MVC

 The async point is placed in between the executing and executed events. When the action invoker notifies that it’s about to execute the action, the thread engaged is still the original ASP.NET thread that picked up the request from the Web server queue. At this point, the action is executed. At the end, when the action invoker is ready to notify the action-executed event, possibly another ASP.NET thread is taking care of the request. Figure 2 illustrates this scenario.


Figure 2 Thread Switching for an Async Action Method Call

Before I discuss the details of how you create and debug asynchronous methods, another fundamental point of asynchronous ASP.NET operations should be made clear: not all actions are good candidates for becoming async operations.

The Real Target of Async Operations

Only I/O-bound operations are good candidates for becoming async action methods on an asynchronous controller class. An I/O-bound operation is an operation that doesn’t depend on the local CPU for completion. When an I/O-bound operation is active, the CPU just waits for data to be processed (that is, downloaded) from external storage (a database or a remote service). I/O-bound operations are in contrast to CPU-bound operations, where the completion of a task depends on the activity of the CPU.

A typical example of an I/O-bound operation is the invocation of a remote service. In this case, the action methods fire the request and then just wait for any response to be downloaded. The real work is being done remotely by another machine and another CPU. Thus, the ASP.NET thread is stuck waiting and being idle. Releasing that idle thread from the duty of waiting to serve other incoming requests is the performance gain you can achieve with async implementation of actions or pages.

It turns out that not all lengthy operations will give you a concrete benefit if implemented asynchronously. A lengthy in-memory calculation doesn’t significantly benefit from asynchronous implementation. It could even run slightly slower, because the same CPU is serving both the ASP.NET request and the calculation. In addition, you may still need an ASP.NET thread to physically take care of the calculation. There’s little benefit, if any, in using the async implementation for CPU-bound operations. On the other hand, if remote resources are involved, even multiple 
resources, using async methods can really boost the performance of the application, if not the performance of the individual request.

I’ll return to this point shortly with an example. For now, let’s focus on the syntax required to define and execute async actions in ASP.NET MVC.

Recognizing Async Routes

In which way is an async route different from a synchronous route? In MVC Futures, you were asked to use different methods to register synchronous and asynchronous routes. Here’s the old way to register an async route:

routes.MapAsyncRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = "" }
);

You had to use the MapAsyncRoute extension method instead of the standard MapRoute you would have used for classic synchronous methods. In ASP.NET MVC 2 RC, however, this distinction has been removed. Now you have just one way to register your routes—the MapRoute method—regardless of how the action will then be executed.

The URL of the request is therefore processed as usual and the name of the controller class to use is figured out. It is required, in fact, that an async method is defined on a controller class that derives from the new AsyncController class, illustrated here:

public class TestController : AsyncController
{
  ...
}

If the controller class inherits from AsyncController, the convention for mapping action names to methods is a bit different. An AsyncController class can serve both synchronous and asynchronous requests. As a result, the convention used can recognize both a method Run and a method RunAsync, as shown here:

public class TestController : AsyncController
{
  public ActionResult Run(int id) 
  {
     ...
  }
  public void RunAsync(int id) 
  {
     ...
  }
}

If you do this, however, an exception will be thrown (see Figure 3).

An async action is identified by name, and the expected pattern is xxxAsync, where xxx indicates the default name of the action to execute. Clearly, if another method named xxx exists, and it is not disambiguated using attributes, then an exception is thrown as in Figure 3.


Figure 3 Ambiguous References in the Name of the Action

The word Async is considered a suffix. The URL to invoke the RunAsync method will contain only the prefix Run. For example, the following URL will invoke the method RunAsync, passing a value of 5 as a route parameter:

https://myserver/demo/run/5

Whether this will be resolved as a synchronous or asynchronous action depends on the methods you have in the AsyncController class. The xxxAsync method, however, identifies only the trigger of the operation. The finalizer of the request is another method in the controller class named xxxCompleted:

public ActionResult RunCompleted(DataContainer data)
{
    ...
}

Note the different signature of the two methods defining the async action. The trigger is expected to be a void method. If you define it to return any value, the return value simply will be ignored. The input parameters of the xxxAsync method will be subject to model binding as usual. The finalizer method returns an ActionResult object as usual and it receives a custom object that contains the data it’s expected to process and pass on to the view object. A special protocol is necessary for matching the values calculated by the trigger to the parameters declared by the finalizer.

The AsyncController Class

The AsyncController controller class inherits from Controller and implements a bunch of new interfaces as shown here:

public abstract class AsyncController : Controller, 
                IAsyncManagerContainer, 
IAsyncController, IController

The most distinctive aspect of an async controller is the special action invoker object that is employed under the hood to perform operations. The invoker needs a counter to track the number of individual operations that compose the action and that must be synchronized before the overall action can be declared terminated. Figure 4 provides a sample implementation for an async action.

Figure 4 A Simple Asynchronous Action Method

public void RunAsync(int id) 
{
    AsyncManager.OutstandingOperations.Increment();
    var d = new DataContainer();
     ...
            
    // Do some remote work (i.e., invoking a service)
     ...
    // Terminate operations
    AsyncManager.Parameters["data"] = d;
    AsyncManager.OutstandingOperations.Decrement();
}
public ActionResult RunCompleted(DataContainer data)
{
   ...
}

The OutstandingOperations member on the AsyncManager class provides a container that maintains a count of pending asynchronous operations. It’s an instance of the OperationCounter helper class and supplies an ad hoc API to increment and decrement. The Increment method is not limited to unary increments, as shown here:

AsyncManager.OutstandingOperations.Increment(2);
service1.GetData(...);
AsyncManager.OutstandingOperations.Decrement();
service2.GetData(...);
AsyncManager.OutstandingOperations.Decrement();

The AsyncManager Parameters dictionary is used to group values to be passed as arguments to the finalizer method of the async call. The Parameters dictionary is expected to contain an entry for each parameter to be passed to the finalizer—the xxxCompleted method in the earlier example. If a match can’t be found between entries in the dictionary and parameter names, a default value is assumed for the parameter—null for reference types. No exception is raised unless an attempt is made to access a null object. The xxxCompleted method receives parameters of any supported type and uses them to fill up the ViewData collection or any strong-typed object recognized by the view. The xxxCompleted method is responsible for returning an ActionResult object.

A Good Fit or Not?

Wrapping up, synchronous requests are a necessary feature in ASP.NET and, in fact, asynchronous HTTP handlers have been supported since ASP.NET 1.0.

ASP.NET Web Forms and ASP.NET MVC offer higher-level tools to code async operations, each within their own application model—in ASP.NET MVC, you have async controllers, and in Web Forms you rely on async pages. 

The key aspect of async actions, though, is deciding whether a given task is a good fit for an asynchronous implementation. Async methods should only be built around I/O-bound operations. And, finally, bear in mind that async methods won’t run faster themselves, but will allow other requests to run faster.

Dino Esposito is the author of the upcoming “Programming ASP.NET MVC” from Microsoft Press and has coauthored “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Based in Italy, Esposito is a frequent speaker at industry events worldwide.

Thanks to the following technical expert for reviewing this article: Stefan Schackow