基准测试异步等待不理解
本文关键字:不理解 等待 异步 基准测试 | 更新日期: 2023-09-27 18:36:11
>我正在使用使用 C#5 的异步/await 结构编写的简单 C# 控制台应用程序进行一些基准测试,并且数字不加起来(事实上它们加起来,这就是问题所在;))
我正在对三种不同的场景进行基准测试:1) 对 SQL 服务器存储过程的 20K 调用。2) 对简单 HTTP 服务器的 20K 调用3) 方案 1) 和 2) 一起
以下是有关方案的更多详细信息:
1) 对 SQL 服务器存储过程的 20K 调用。
在此方案中,我调用外部 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) 对简单 HTTP 服务器的 20K 调用
在这种情况下,我也使用 HttpClient 和异步方法(PostAsync)调用外部 Web 服务器上的 URL。
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%。
现在来回答问题
我不明白为什么基准测试需要 40 秒才能用于场景 #3。如果执行是顺序的,或者我的 CPU(或 I/O)对于场景 1 和 2 是 100%,我会说场景 1 的计时 + 场景 2 的计时是正常的。
考虑到我正在使用 async/await 构造完全异步,我对场景 #3 的期望是让它在 30 秒内完成("链中最薄弱的环节"),即场景 #2 的持续时间。
这里有一些我不明白的地方:(
有什么线索吗?
编辑:根据@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 和场景 2 的结果来看,我猜对数据库的查询比 HTTP 请求完成得更快(当然,因为本地磁盘的延迟比互联网少)。因此,它们通常会在 10 秒内完成。但是你提交的任务中有一半是HTTP请求,所以你的一半计算能力都忙于此,数据库所需的时间从10秒增加到20秒。在此期间,CPU 使用率为 0.7,这是可实现的最大使用率(因此池实际上是有效的,因为它最大化了资源利用率,即使 50% 的任务仅使用 30% 的 CPU)。
然后,数据库请求完成,只剩下 HTTP 请求。虽然 DB 和 HTTP 是同时执行的,但只有一半的资源专用于 HTTP,因此这 20 秒等于 10 秒的无争用执行,这还剩下其他 30 秒到 10 秒 = 20 秒的执行量。
即使 CPU 利用率不是 100%,任务管理器也不会创建 40k 个线程,因为这会破坏操作系统。