ASP.NET C#5 Asynchronous Web Applications Using As

2019-03-10 03:56发布

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.

1条回答
叛逆
2楼-- · 2019-03-10 04:16

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.

查看更多
登录 后发表回答