简体   繁体   中英

Why is this implementation of async WebRequests slower than the synchronous implementation?

I have an application that requires many requests to a third party REST service. I thought that modifying this part of the application to make the requests asynchronously would potentially speed things up, so I wrote a POC console application to test things out.

To my surprise the async code takes almost twice as long to complete as the synchronous version. Am I just doing it wrong?

async static void LoadUrlsAsync() 
{
    var startTime = DateTime.Now;
    Console.WriteLine("LoadUrlsAsync Start - {0}", startTime);


    var numberOfRequest = 3;
    var tasks = new List<Task<string>>();

    for (int i = 0; i < numberOfRequest; i++)
    {
        var request = WebRequest.Create(@"http://www.google.com/images/srpr/logo11w.png") as HttpWebRequest;
        request.Method = "GET";

        var task = LoadUrlAsync(request);
        tasks.Add(task);
    }

    var results = await Task.WhenAll(tasks);

    var stopTime = DateTime.Now;
    var duration = (stopTime - startTime);
    Console.WriteLine("LoadUrlsAsync Complete - {0}", stopTime);
    Console.WriteLine("LoadUrlsAsync Duration - {0}ms", duration);
}

async static Task<string> LoadUrlAsync(WebRequest request)
{
    string value = string.Empty;
    using (var response = await request.GetResponseAsync())
    using (var responseStream = response.GetResponseStream())
    using (var reader = new StreamReader(responseStream))
    {
        value = reader.ReadToEnd();
        Console.WriteLine("{0} - Bytes: {1}", request.RequestUri, value.Length);
    }

    return value;
}

NOTE: I have also tried setting the maxconnections=100 in the app.config in an attempt to eliminate throttling from the system.net connection pool. This setting doesn't seem to make an impact on the performance.

  <system.net>
    <connectionManagement>
      <add address="*" maxconnection="100" />
    </connectionManagement>
  </system.net>

First, try to avoid microbenchmarking. When the differences of your code timings are swamped by network conditions, your results lose meaning.

That said, you should set ServicePointManager.DefaultConnectionLimit to int.MaxValue . Also, use end-to-end async methods (ie, StreamReader.ReadToEndAsync ) - or even better, use HttpClient , which was designed for async HTTP.

The async version becomes faster as you increase the number of threads. I'm not certain, however my guess is that you are bypassing the cost of setting up the threads. When you pass this threshold the async version becomes superior. Try 50 or even 500 requests and you should see async is faster. That's how it worked out for me.

500 Async Requests:  11.133 seconds
500 Sync Requests:   18.136 seconds

If you only have ~3 calls then I suggest avoiding async. Here's what I used to test:

public class SeperateClass
{
    static int numberOfRequest = 500;
    public async static void LoadUrlsAsync()
    {
        var startTime = DateTime.Now;
        Console.WriteLine("LoadUrlsAsync Start - {0}", startTime);

        var tasks = new List<Task<string>>();

        for (int i = 0; i < numberOfRequest; i++)
        {
            var request = WebRequest.Create(@"http://www.google.com/images/srpr/logo11w.png") as HttpWebRequest;
            request.Method = "GET";

            var task = LoadUrlAsync(request);
            tasks.Add(task);
        }

        var results = await Task.WhenAll(tasks);

        var stopTime = DateTime.Now;
        var duration = (stopTime - startTime);
        Console.WriteLine("LoadUrlsAsync Complete - {0}", stopTime);
        Console.WriteLine("LoadUrlsAsync Duration - {0}ms", duration);
    }

    async static Task<string> LoadUrlAsync(WebRequest request)
    {
        string value = string.Empty;
        using (var response = await request.GetResponseAsync())
        using (var responseStream = response.GetResponseStream())
        using (var reader = new StreamReader(responseStream))
        {
            value = reader.ReadToEnd();
            Console.WriteLine("{0} - Bytes: {1}", request.RequestUri, value.Length);
        }

        return value;
    }
}

public class SeperateClassSync
{
    static int numberOfRequest = 500;
    public async static void LoadUrlsSync()
    {
        var startTime = DateTime.Now;
        Console.WriteLine("LoadUrlsSync Start - {0}", startTime);

        var tasks = new List<Task<string>>();

        for (int i = 0; i < numberOfRequest; i++)
        {
            var request = WebRequest.Create(@"http://www.google.com/images/srpr/logo11w.png") as HttpWebRequest;
            request.Method = "GET";

            var task = LoadUrlSync(request);
            tasks.Add(task);
        }

        var results = await Task.WhenAll(tasks);

        var stopTime = DateTime.Now;
        var duration = (stopTime - startTime);
        Console.WriteLine("LoadUrlsSync Complete - {0}", stopTime);
        Console.WriteLine("LoadUrlsSync Duration - {0}ms", duration);
    }

    async static Task<string> LoadUrlSync(WebRequest request)
    {
        string value = string.Empty;
        using (var response = request.GetResponse())//Still async FW, just changed to Sync call here
        using (var responseStream = response.GetResponseStream())
        using (var reader = new StreamReader(responseStream))
        {
            value = reader.ReadToEnd();
            Console.WriteLine("{0} - Bytes: {1}", request.RequestUri, value.Length);
        }

        return value;
    }
}

class Program
{
    static void Main(string[] args)
    {
        SeperateClass.LoadUrlsAsync();
        Console.ReadLine();//record result and run again

        SeperateClassSync.LoadUrlsSync();
        Console.ReadLine();
    }
}

In my tests it's faster to use the WebRequest.GetResponseAsync() method for 3 parallel requests.

It should be more noticeable with large requests, many requests (3 is not many), and requests from different websites.

What are the exact results you are getting? In your question you are converting a TimeSpan to a string and calling it milliseconds but you aren't actually calculating the milliseconds. It's displaying the standard TimeSpan.ToString which will show fractions of a second.

It appears that the problem was more of an environmental issue than anything else. Once I moved the code to another machine on a different network, the results were much more inline with my expectations.

The original async code does in fact execute more quickly than the synchronous version. This helps me ensure that I am not introducing additional complexity to our application without the expected performance gains.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM