我发现这个问题非常有趣,特别是因为我在 Ado.Net 和 EF 6 的任何地方都使用async。我希望有人对这个问题给出解释,但它没有发生。所以我试图在我这边重现这个问题。我希望你们中的一些人会觉得这很有趣。
第一个好消息:我复制了它:) 差异是巨大的。系数为 8 ...
首先,我怀疑与 CommandBehavior 有什么关系,因为 I read an interesting article 与 Ado 关于 async 的关系是这样的:
"由于非顺序访问模式必须存储整行的数据,如果您从服务器读取大列(例如 varbinary(MAX)、varchar(MAX)、nvarchar(MAX ) 或 XML)。”
我怀疑 ToList() 调用是 CommandBehavior.SequentialAccess 和异步调用是 CommandBehavior.Default (非顺序的,这可能会导致问题)。因此,我下载了 EF6 的源代码,并在各处放置断点(当然,在使用 CommandBehavior 的地方)。
结果:没有。所有的调用都是用CommandBehavior.Default 进行的......所以我试图进入 EF 代码以了解会发生什么......而且......哦哦......我从来没有见过这样的委托代码,一切似乎都懒惰执行......
所以我尝试进行一些分析以了解发生了什么......
而且我觉得我有一些东西......
这是创建我进行基准测试的表的模型,其中包含 3500 行,每个 varbinary(MAX) 中有 256 Kb 随机数据。 (EF 6.1 - CodeFirst - CodePlex):
public class TestContext : DbContext
{
public TestContext()
: base(@"Server=(localdb)\\v11.0;Integrated Security=true;Initial Catalog=BENCH") // Local instance
{
}
public DbSet<TestItem> Items { get; set; }
}
public class TestItem
{
public int ID { get; set; }
public string Name { get; set; }
public byte[] BinaryData { get; set; }
}
这是我用来创建测试数据和基准 EF 的代码。
using (TestContext db = new TestContext())
{
if (!db.Items.Any())
{
foreach (int i in Enumerable.Range(0, 3500)) // Fill 3500 lines
{
byte[] dummyData = new byte[1 << 18]; // with 256 Kbyte
new Random().NextBytes(dummyData);
db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData });
}
await db.SaveChangesAsync();
}
}
using (TestContext db = new TestContext()) // EF Warm Up
{
var warmItUp = db.Items.FirstOrDefault();
warmItUp = await db.Items.FirstOrDefaultAsync();
}
Stopwatch watch = new Stopwatch();
using (TestContext db = new TestContext())
{
watch.Start();
var testRegular = db.Items.ToList();
watch.Stop();
Console.WriteLine("non async : " + watch.ElapsedMilliseconds);
}
using (TestContext db = new TestContext())
{
watch.Restart();
var testAsync = await db.Items.ToListAsync();
watch.Stop();
Console.WriteLine("async : " + watch.ElapsedMilliseconds);
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
while (await reader.ReadAsync())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReaderAsync SequentialAccess : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
while (await reader.ReadAsync())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReaderAsync Default : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
while (reader.Read())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReader SequentialAccess : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = cmd.ExecuteReader(CommandBehavior.Default);
while (reader.Read())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReader Default : " + watch.ElapsedMilliseconds);
}
}
对于常规的 EF 调用 (.ToList()),分析看起来“正常”并且易于阅读:
在这里,我们找到了秒表的 8.4 秒(分析会减慢性能)。我们还发现沿调用路径的 HitCount = 3500,这与测试中的 3500 行一致。在 TDS 解析器方面,事情开始变得更糟,因为我们在 TryReadByteArray() 方法上读取了 118 353 次调用,这就是缓冲循环发生的原因。 (256kb 的每个 byte[] 平均调用 33.8 次)
对于async 的情况,真的很不一样......首先,.ToListAsync() 调用被安排在 ThreadPool 上,然后等待。这里没有什么了不起的。但是,现在,这是 ThreadPool 上的 async 地狱:
首先,在第一种情况下,我们在整个调用路径中只有 3500 个命中计数,这里我们有 118 371 个。此外,您必须想象我没有放在屏幕截图上的所有同步调用......
其次,在第一种情况下,我们对 TryReadByteArray() 方法的调用“只有 118 353 次”,这里我们有 2 050 210 次调用!它是 17 倍……(在一个 1Mb 大阵列的测试中,它是 160 倍)
还有:
- 已创建 120 000 个
Task 实例
- 727 519
Interlocked 来电
- 290 569
Monitor 致电
- 98 283
ExecutionContext 实例,有 264 481 次捕获
- 208 733
SpinLock 电话
我的猜测是缓冲是以异步方式进行的(而且不是一种好的方式),并行任务试图从 TDS 读取数据。创建太多任务只是为了解析二进制数据。
作为初步结论,我们可以说 Async 很棒,EF6 很棒,但是 EF6 在其当前实现中使用异步增加了主要开销,在性能方面、线程方面和 CPU 方面(12% CPU ToList() 案例中的使用率和 ToListAsync 案例中的 20% 的使用时间延长了 8 到 10 倍......我在旧的 i7 920 上运行它)。
在做一些测试时,我在想this article again,我注意到我想念的东西:
“对于 .Net 4.5 中的新异步方法,它们的行为与同步方法完全相同,除了一个值得注意的例外:非顺序模式下的 ReadAsync。”
什么?!!!
所以我扩展了我的基准测试,将 Ado.Net 包括在常规/异步调用中,并使用 CommandBehavior.SequentialAccess / CommandBehavior.Default,这是一个很大的惊喜! :
我们与 Ado.Net 的行为完全相同!!!捂脸……
我的最终结论是:EF 6 实现中存在错误。当对包含binary(max) 列的表进行异步调用时,它应该将CommandBehavior 切换为SequentialAccess。创建太多任务,减慢进程的问题是在 Ado.Net 方面。 EF 的问题是它没有按应有的方式使用 Ado.Net。
现在您知道,与其使用 EF6 异步方法,不如以常规的非异步方式调用 EF,然后使用 TaskCompletionSource<T> 以异步方式返回结果。
注意 1 :由于一个可耻的错误,我编辑了我的帖子....这是更新的结果。
注意 2:我没有将测试扩展到其他用例(例如:nvarchar(max) 有大量数据),但有可能发生相同的行为。
注 3:ToList() 的情况通常是 12% CPU(我的 CPU 的 1/8 = 1 个逻辑核心)。不寻常的是ToListAsync() 案例的最大 20%,好像调度程序无法使用所有 Treads。可能是创建的Task太多,也可能是TDS解析器的瓶颈,我不知道...