基准测试异步等待不理解

时间:2013-10-19 00:42:29

标签: c# async-await

我正在使用一个简单的C#控制台应用程序进行一些基准测试,该应用程序使用C#5的async / await构造编写,并且数字不加起来(事实上它们加起来,这就是问题;)

我正在对三种不同的场景进行基准测试: 1)20K调用SQL服务器存储过程。 2)20K调用简单的HTTP服务器 3)场景1)和2)一起

以下是有关方案的更多详细信息:

1)20K调用SQL Server存储过程。

在这种情况下,我调用外部SQL Server存储过程20K次。 CallSqlStoredProcedureAsync方法使用ADO.NET异步方法(OpenAsync,ExecuteNonQueryAsync ...)。我甚至在方法入口处等待Task.Yield()以避免在到达异步点之前执行任何同步代码,以避免在最轻微的时间内阻塞我的循环。

var tasks = new List<Task>();

for (int i=0; i<20000; i++)
{
    tasks.Add(this.CallSqlStoredProcedureAsync());
}

Task.WhenAll(tasks).Wait();

大约 10秒,CPU平均耗电量为70%。

2)20K调用简单的HTTP服务器

在这种情况下,我使用HttpClient和异步方法调用外部Web服务器上的URL(PostAsync)。

for (int i=0; i<20000; i++)
{
    tasks.Add(this.SendRequestToHttpServerAsync());
}

大约 30秒,CPU平均耗电率为30%

3)场景1)和2)一起

for (int i=0; i<20000; i++)
{
    tasks.Add(this.CallSqlStoredProcedureAsync());
    tasks.Add(this.SendRequestToHttpServerAsync());
}

大约 40秒完成,CPU消耗平均值为70%,持续约20秒,然后剩余20秒消耗30%。

现在提问

我不明白为什么基准测试为场景#3需要40秒。如果执行是顺序的,或者我的CPU(或I / O)对于场景1和场景2来说是100%,我会说场景1的时间+场景2的时间是正常的。

考虑到我使用async / await构造完全异步,我期望场景#3在30秒内完成(“链中最弱的链接”),即场景#2的持续时间。< / p>

这里有一些我不理解的东西:(

有任何线索吗?

编辑:根据@svick请求,这里是基准测试的完整代码(不包括一些无用的东西)

 static void Main(string[] args)
 {
     var bench = new Bench();         

     while (true)
     {
         string iterationsAndScenario = Console.ReadLine();
         var iterations = int.Parse(iterationsAndScenario.Split(' ')[0]);
         var scenario = int.Parse(iterationsAndScenario.Split(' ')[1]);

         var sw = new Stopwatch();
         sw.Start();
         bench.Start(iterations, scenario).Wait();
         sw.Stop();

         Console.WriteLine("Bench too {0} ms", sw.EllapsedMilliseconds);
    }
}

public class Benchmark
{
    public Task Start(int iterations, int scenario)
    {
        var tasks = new List<Task>();

        if (scenario == 1)
        {
            for (int i=0; i<iterations; i++)
            {
                tasks.Add(this.CallSqlStoredProcedureAsync().ContinueWith(t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted));
            }
        }
        else if (scenario == 2)
        {
            var httpClient = new HttpClient();

            for (int i=0; i<iterations; i++)
            {
                 tasks.Add(httpClient.PostAsync(uri, new StringContent("Hello")).ContinueWith(t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted));
            }
        }
        else if (scenario == 3)
        {
            var httpClient = new HttpClient();

            for (int i=0; i<iterations; i++)
            {
                 tasks.Add(this.CallSqlStoredProcedureAsync().ContinueWith(t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted));

                 tasks.Add(httpClient.PostAsync(uri, new StringContent("Hello")).ContinueWith(t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted));
            }
        }

        return Task.WhenAll(tasks);
    }

    public async Task CallSqlStoredProcedureAsync()
    {
        await Task.Yield();

        using (var conn = new SqlConnection(connectionString))
        {
            using (var cmd = new SqlCommand("sp_mystoreproc", conn))
            {
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.AddWithValue("@param1", 'A');
                cmd.Parameters.AddWithValue("@param2", 'B');

                await cmd.Connection.OpenAsync();
                await cmd.ExecuteNonQueryAsync(); 
            }
        }
    }
}

1 个答案:

答案 0 :(得分:0)

从方案1和2的结果来看,我认为对数据库的查询比HTTP请求完成得更快(当然,因为本地磁盘的延迟比Internet少)。 所以,它们通常会在10秒内完成。但是你提交的一半任务是HTTP请求,因此你的计算能力的一半都在忙,而DB需要的时间从10s增加到20s。在此期间,CPU使用率为0.7,这是可实现的最大使用量(因此池实际上是有效的,因为它最大化资源利用率,即使50%的任务仅使用30%的CPU)。

然后,数据库请求完成,只剩下HTTP请求。虽然DB和HTTP是同时执行的,但只有一半的资源专用于HTTP,因此这些20s等于10s的无竞争执行,这使得其他30s-10s = 20s的执行。

即使CPU利用率不是100%,任务管理器也不会创建40k线程,因为这会破坏操作系统。