为什么异步 WebRequest 的这种实现比同步实现慢

本文关键字:实现 同步 异步 WebRequest 为什么 | 更新日期: 2023-09-27 18:37:18

我有一个应用程序需要向第三方REST服务发出许多请求。 我认为修改应用程序的这一部分以异步发出请求可能会加快速度,因此我编写了一个 POC 控制台应用程序来测试。

令我惊讶的是,异步代码完成的时间几乎是同步版本的两倍。 我只是做错了吗?

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;
}

注意:我还尝试在 app.config 中设置 maxconnections=100,以尝试消除 system.net 连接池的限制。 此设置似乎不会影响性能。

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

为什么异步 WebRequest 的这种实现比同步实现慢

首先,尽量避免微观基准测试。当代码时序的差异被网络条件淹没时,结果将失去意义。

也就是说,您应该ServicePointManager.DefaultConnectionLimit设置为 int.MaxValue .此外,使用端到端async方法(即StreamReader.ReadToEndAsync) - 甚至更好的是,使用HttpClient,这是为async HTTP设计的。

随着线程数的增加,异步版本会变得更快。我不确定,但我的猜测是你绕过了设置线程的成本。当您超过此阈值时,异步版本将变得更好。尝试 50 个甚至 500 个请求,您应该会看到异步更快。 这就是它对我来说的方式。

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

如果你只有~3个调用,那么我建议避免异步。 这是我用来测试的内容:

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();
    }
}

在我的测试中,对 3 个并行请求使用 WebRequest.GetResponseAsync() 方法更快。

对于大请求、许多请求(3 个不多)和来自不同网站的请求,它应该更明显。

你得到的确切结果是什么? 在您的问题中,您正在将 TimeSpan 转换为字符串并称其为毫秒,但您实际上并没有计算毫秒。 它显示标准的TimeSpan.ToString,它将显示几分之一秒。

看来这个问题更像是一个环境问题,而不是其他任何事情。 一旦我将代码移动到不同网络上的另一台机器上,结果就更符合我的期望。

实际上,原始异步代码的执行速度确实比同步版本快。 这有助于我确保不会在没有预期性能提升的情况下给我们的应用程序带来额外的复杂性。