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.