Async Pages In ASP.NET 2.0

Friday, June 17, 2005

Fritz Onion asks an interesting question in his “Value of asynch tasks” post. Do asynch tasks add any benefit for parallelizing asynchronous web service invocations? I’ve been experimenting with the feature too, and wanted to offer an answer.

Let’s say we need to call a HelloWorld web service that returns a string, but the service takes 5 seconds to complete – and we need to call it twice. In the simplest case we are looking at a 10 second response time. We might try to improve response time by kicking off simultaneous calls the service like so:

    1     protected void Page_Load(object sender, EventArgs e)

    2     {

    3         IAsyncResult ar1 = helloService.BeginHelloWorld(null, null);

    4         IAsyncResult ar2 = helloService.BeginHelloWorld(null, null);

    5 

    6         TextBox1.Text = helloService.EndHelloWorld(ar1);

    7         TextBox2.Text = helloService.EndHelloWorld(ar2);

    8     }

The response time for the page will be just over 5 seconds – a great improvement - but at what cost? We are tying up 3 threads to process 1 user request. This approach might work well for apps with low utilization, but I’d want to do some careful stress testing before this page sees a heavy load.

ASP.NET 2.0 introduces asynch pages. With asynch pages you register one or more tasks with the RegisterAsyncTask method for the runtime to execute asynchronously. RegisterAsyncTask requires a PageAsyncTask object initialized with begin, end, and timeout event handlers for the async task. You can also pass along a state object, and indicate if the task should execute in parallel. With anonymous delegates, you could put together code like the following:

    1 protected void Page_Load(object sender, EventArgs e)

    2 {

    3     PageAsyncTask task1;

    4     PageAsyncTask task2;

    5 

    6     bool executeInParallel = true;

    7 

    8     task1 = new PageAsyncTask

    9         (

   10             delegate(Object source, EventArgs ea, AsyncCallback callback, Object state)

   11                 { return helloService.BeginHelloWorld(SLEEPTIME, callback, state); },

   12 

   13             delegate(IAsyncResult ar)

   14                 { TextBox1.Text = helloService.EndHelloWorld(ar); },

   15             // dont' need a TimeOut handler, or state object

   16             null, null, executeInParallel

   17         );

   18 

   19     task2 = new PageAsyncTask

   20         (

   21             delegate(Object source, EventArgs ea, AsyncCallback callback, Object state)

   22                 { return helloService.BeginHelloWorld(SLEEPTIME, callback, state); },

   23 

   24             delegate(IAsyncResult ar)

   25                 { TextBox2.Text = helloService.EndHelloWorld(ar); },

   26 

   27             null, null, executeInParallel

   28         );       

   29 

   30     RegisterAsyncTask(task1);

   31     RegisterAsyncTask(task2);       

   32 }

A couple notes about the above code. First, it does the same job as the first code sample but appears longer and complex. However, the amount of code doesn’t tell the whole story – we’ll slowly uncover more. The runtime will ensure the registered tasks either complete or timeout before the page goes to render. We can run the tasks in parallel by passing a true value for the last parameter to the ctor for PageAsyncTask (the default is false). Here is one advantage to the Async page model – we can move from parallel to serialized processing by toggling one Boolean variable.

Another advantage to async pages is a feature we are not taking advantage of – the timeout delegate (third parameter in the PageAsyncTask ctor). If a task takes too long to complete, the runtime can move ahead and finish processing the page, optionally notifying us about the timeout if we pass a third delegate to the task. This is another feature that would be difficult to construct in the first code example.

I was also curious to see if async pages did a better job managing all the threads involved. The threads spend most of their time waiting 5 seconds for a web service method return. I whipped up a quick stress test using the testing tools in VS 2005 Beta 2 (which rock, by the way - very easy to use, and I’ve used a fair number of web stress test tools). Here are some average results after simulating a 5 user load for 5 minutes.

Avg Requests per Second Avg Request Time (s)
First code sample 2.9 5.0
Async page (parallel=true) 2.9 5.0
Async page (parallel=false) 1.5 11.1

This is roughly what we would expect. Both parallel approaches are pretty even, and by toggling the executeInParallel flag we double the response time for our async page. Let’s try again with a 20 user simulated load.

Request / sec Request time (s)
First code sample 2.3 23.4
Async page (parallel=true) 3.5 15.5
Async page (parallel=false) 3.5 14.4

Under load, the async page sustains a higher throughput, primarily because, I believe, the incoming request thread is free to go back to the worker pool once the async tasks are registered and the begin handlers fire. This is unlike the first code sample, where the primary request thread hangs around waiting for the async calls to complete.

I’ll follow up with some thread tracing analysis soon.

I think Async pages are a great addition to 2.0 when used in the right situations. You can easily control the parallelizing of tasks, easily handle timeouts, and thread management is superior compared to the simple approach in the first code snippet.


Comments
Fred H Friday, June 17, 2005
This is some great insight! Thanks!
Marcus Mac Innes Sunday, June 19, 2005
I'd be very interested in a comparison of performance increases comparing using the new async features in 2.0 with simply increasing the ASP.NET thread count... Have you experimented with comparing the two yet? I bet actual figures would be fairly close, which would then bring into question whether the extra coding efforts/complexity to use the async features would be worth it.
Scott Sunday, June 19, 2005
That would be an interesting test, I've thought of about 20 experiments to try after posting this - all I need is some extra time to try them all out!
Fritz Onion Monday, June 20, 2005
It is important to note that there is really no difference between the first approach and using Async tasks *unless* you mark the page as async. The async web service invocations work exactly the same way, and will use threads from the I/O theradpool (whereas ASP.NET uses threads from the worker threadpool). The advantage is definitely in marking the page async which relinquishes the worker thread that is servicing the request to work on other requests (hence the higher throughput under stress). The reason this works at all is that you are using async web requests that use the I/O threadpool - for example, if you used async delegates you would be releasing one thread back to the threadpool but then re-acquiring another thread from the same pool for a net gain of 0.
<br>In response to the question on increasing the threadpool count - that will work up to a point, but the advantage of being able to mark individual pages as async and having them served on alternate threads is that you know they will be performing I/O-bound work that will artificially constrain the threadpool. You can always throw more requests at the process and eat up the extra threads which will plateau quickly.
Rick Strahl Wednesday, July 6, 2005
All the examples of this I've seen use either HttpWebRequest or an async Web Service, both of which return IAsyncResults in the first place.

So what do you do if you need to some processing of your own and you have to set up your own AsyncRequest structure to return? Do you still get the benefits?

Also, in Beta 2 it looks like the command line compiler doesn't like Async="true" in ASPX page code, but the code runs fine when compiled on the fly.
scott Thursday, July 7, 2005
I think in that case you'd need to define a delegate type to surround your own work with BeginInvoke, EndInvoke....

public partial class _Default : Page
{
protected void Page_Load(object sender,
EventArgs e)
{
PageAsyncTask task;
worker = new DoWorkDelegate(DoWork);
task = new PageAsyncTask(
BeginDoWork,
EndDowork,
null, null
);

RegisterAsyncTask(task);
}

protected IAsyncResult BeginDoWork(
object sender,
EventArgs e,
AsyncCallback cb,
object extraData)
{
return worker.BeginInvoke(
3, cb, extraData
);
}

protected void EndDowork(IAsyncResult ar)
{
TextBox1.Text = worker.EndInvoke(ar);
}

protected string DoWork(int param)
{
// ...
// ...
// ...
return (param * param).ToString();
}

DoWorkDelegate worker;
private delegate string
DoWorkDelegate(int param);
}
Eron Wright Sunday, October 9, 2005
I believe that the worker thread pool is fixed at 25 threads in .NET 1.1, despite any documentation to the contrary. I have looked at Mono source and it shows the same thing. Might be different story under 2.0.

I have confirmed that the scalability of async is a world better than sync.

The Hotmail team confirmed at PDC2005 that all pages are implemented as async. They debated playing with threads, but decided that the default was sufficient.
gravatar r4 nds Tuesday, October 13, 2009
Thanxfor the info.....what do you do if you need to some processing of your own and you have to set up your own AsyncRequest structure to return?
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!