Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET C#5 Asynchronous Web Applications Using Async & Await

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.

like image 899
Paul Mooney Avatar asked Jun 17 '13 10:06

Paul Mooney


People also ask

What is ASP.NET C?

ASP.NET is a web application framework developed and marketed by Microsoft to allow programmers to build dynamic web sites. It allows you to use a full featured programming language such as C# or VB.NET to build web applications easily.

Is ASP.NET like C#?

ASP.NET is a web application development framework used to develop web applications using different back-end programming languages like C# where C# is used as an object-oriented programming language to develop web applications along with ASP.NET.

Can I use .NET with C?

. NET Framework is an object oriented programming framework meant to be used with languages that it provides bindings for. Since C is not an object oriented language it wouldn't make sense to use it with the framework.

What is .NET ASP.NET and C#?

Basically, ASP.NET is a web delivery mechanism that runs either C# or VB.NET in the background. C# is a programming language that runs ASP.NET as well as Winforms, WPF, and Silverlight.


1 Answers

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.

like image 195
Stephen Cleary Avatar answered Oct 09 '22 10:10

Stephen Cleary