10

Having researched the concept of asynchronous web development, specifically from this source, I created a sample application to prove the concept.

The solution is composed of 2 ASP.NET Web API applications. The first is a simulated slow endpoint; it waits for 1000 ms before returning a list a custom class called Student:

 public IEnumerable<Student> Get()
 {
 Thread.Sleep(1000);
 return new List<Student> { new Student { Name = @"Paul" }, new Student { Name = @"Steve" }, new Student { Name = @"Dave" }, new Student { Name = @"Sue" } };
 }

Here is the Student class:

public class Student
{
 public string Name { get; set; }
}

This endpoint is hosted in IIS 7 on localhost:4002.

The second application contacts the first using 2 endpoints, one synchronous, the other asynchronous:

public IEnumerable<Student> Get() {
 var proxy = WebRequest.Create(@"http://localhost:4002/api/values");
 var response = proxy.GetResponse();
 var reader = new StreamReader(response.GetResponseStream());
 return JsonConvert.DeserializeObject<IEnumerable<Student>>(reader.ReadToEnd());
 }
 public async Task<IEnumerable<Student>> Get(int id) {
 var proxy = new HttpClient();
 var getStudents = proxy.GetStreamAsync(@"http://localhost:4002/api/values");
 var stream = await getStudents;
 var reader = new StreamReader(stream);
 return JsonConvert.DeserializeObject<IEnumerable<Student>>(reader.ReadToEnd());
 }

It's hosted in IIS 7 on localhost:4001.

Both endpoints work as expected, and return in approx. 1 second. Based on the video in the link above at 13:25, the asynchronous method should release it's Thread, minimizing contention.

I'm running performance tests on the application using Apache Bench. Here are the response times for the synchronous method with 10 concurrent requests:

Synchronous Results

This is much as I'd expect; more concurrent connections increase contention and extend the response times. However, here are the asynchronous response times:

Asynchronous Results

As you can see, there still seems to be some contention. I would have expected the average response times to be more balanced. If I run the tests on both endpoints with 50 concurrent requests, I still get similar results.

Based on this, it seems that both asynchronous and synchronous methods are running at more or less the same speed (expected), not taking into account the overhead in asynchronous methods, but also that the asynchronous method doesn't seem to be releasing Threads back to the ThreadPool. I'd welcome any comments or clarifications, thanks.

asked Jun 17, 2013 at 10:10
11
  • The biggest change you will get is when your methods rely on external sources, such as database. Commented Jun 17, 2013 at 10:14
  • Thanks, I would have thought that this was the case for any long-running request, which is what I've simulated. Are you suggesting that the async-framework is optimised for IO-based requests as opposed to CPU-bound? I'll modify the code to include a database request. Commented Jun 17, 2013 at 10:22
  • 1
    The point of the async-framework is to release the Thread when it has to wait for a response, so it can take care of other work meanwhile. This is most of the time for IO-based requests the case. When you request much data from the database, what should the thread to meanwhile? Just wait until the data is back... Or return to the threadpool meanwhile and work on other stuff until the data is back? Commented Jun 17, 2013 at 10:32
  • Perhaps you should make your server async as well? Return Task<IEnumerable<Student>> instead? Commented Jun 17, 2013 at 10:47
  • 1
    Also, your async method is actually not fully asynchronous, you should use ReadToEndAsync(). Commented Jun 17, 2013 at 11:26

1 Answer 1

8

I think there's a pretty good chance you're not testing what you think you're testing. From what I can gather, you're trying to detect releases back to the thread pool by comparing timings and deducing thread injection.

For one thing, the default settings for the thread pool on .NET 4.5 are extremely high. You're not going to hit them with just 10 or 100 simultaneous requests.

Step back for a second and think of what you want to test: does an async method return its thread to the thread pool?

I have a demo that I show to demonstrate this. I didn't want to create a heavy load test for my demo (running on my presentation laptop), so I pulled a little trick: I artificially restrict the thread pool to a more reasonable value.

Once you do that, your test is quite simple: perform that many simultaneous connections, and then perform that many plus one. The synchronous implementation will have to wait for one to complete before starting the last one, while the asynchronous implementation will be able to start them all.

On the server side, first restrict the thread pool threads to the number of processors in the system:

protected void Application_Start()
{
 int workerThreads, ioThreads;
 ThreadPool.GetMaxThreads(out workerThreads, out ioThreads);
 ThreadPool.SetMaxThreads(Environment.ProcessorCount, ioThreads);
 ...
}

Then do the synchronous and asynchronous implementations:

public class ValuesController : ApiController
{
 // Synchronous
 public IEnumerable<string> Get()
 {
 Thread.Sleep(1000);
 return new string[] { "value1", "value2" };
 }
 // Asynchronous
 public async Task<IEnumerable<string>> Get(int id)
 {
 await Task.Delay(1000);
 return new string[] { "value1", "value2" };
 }
}

And finally the client testing code:

static void Main(string[] args)
{
 try
 {
 MainAsync().Wait();
 }
 catch (Exception ex)
 {
 Console.WriteLine(ex);
 }
 Console.ReadKey();
}
static async Task MainAsync()
{
 ServicePointManager.DefaultConnectionLimit = int.MaxValue;
 var sw = new Stopwatch();
 var client = new HttpClient();
 var connections = Environment.ProcessorCount;
 var url = "http://localhost:35697/api/values/";
 await client.GetStringAsync(url); // warmup
 sw.Start();
 await Task.WhenAll(Enumerable.Range(0, connections).Select(i => client.GetStringAsync(url)));
 sw.Stop();
 Console.WriteLine("Synchronous time for " + connections + " connections: " + sw.Elapsed);
 connections = Environment.ProcessorCount + 1;
 await client.GetStringAsync(url); // warmup
 sw.Restart();
 await Task.WhenAll(Enumerable.Range(0, connections).Select(i => client.GetStringAsync(url)));
 sw.Stop();
 Console.WriteLine("Synchronous time for " + connections + " connections: " + sw.Elapsed);
 url += "13";
 connections = Environment.ProcessorCount;
 await client.GetStringAsync(url); // warmup
 sw.Restart();
 await Task.WhenAll(Enumerable.Range(0, connections).Select(i => client.GetStringAsync(url)));
 sw.Stop();
 Console.WriteLine("Asynchronous time for " + connections + " connections: " + sw.Elapsed);
 connections = Environment.ProcessorCount + 1;
 await client.GetStringAsync(url); // warmup
 sw.Restart();
 await Task.WhenAll(Enumerable.Range(0, connections).Select(i => client.GetStringAsync(url)));
 sw.Stop();
 Console.WriteLine("Asynchronous time for " + connections + " connections: " + sw.Elapsed);
}

On my (8-logical-core) machine, I see output like this:

Synchronous time for 8 connections: 00:00:01.0194025
Synchronous time for 9 connections: 00:00:02.0362007
Asynchronous time for 8 connections: 00:00:01.0413737
Asynchronous time for 9 connections: 00:00:01.0238674

Which clearly shows that the asynchronous method is returning its thread to the thread pool.

answered Jun 17, 2013 at 13:13

7 Comments

Thanks Stephen, superb explanation. Incidentally, when IIS returns the Thread to the ThreadPool during an async request, what Thread(s) does the resulting State-machine, which manages the callback, run on? Is it hosted by IIS, in which case, does it also leverage the ThreadPool, or is it run on OS Threads outside IIS?
By default, an async method will resume in the request context. What actually happens is that ASP.NET will grab any available thread (from its thread pool) and that thread will enter the request context before resuming the method.
Thanks. I understand that the Thread is returned to the pool until the awaited methods returns. As regards the state machine that monitors the awaited request - does it run in the context of ASP.NET on the ThreadPool? Or is it hosted elsewhere?
What's the purpose of your question? The state machine is the async method (after it has been transformed), and it runs on an ASP.NET thread pool thread, within a request context by default.
The purpose was to confirm in what context the state machine operates, which you've clarified, thanks. It runs in the context of the initial request.
|

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.