entity frameworkasynchronous操作需要十倍的时间才能完成

我有一个使用entity framework6来处理数据库的MVC网站,我一直在尝试改变它,以便所有东西作为asynchronous控制器运行,并且调用数据库作为它们的asynchronous运行(例如,ToListAsync()而不是ToList())

我遇到的问题是,简单地改变我的查询asynchronous已经使他们变得非常慢。

以下代码从我的数据上下文中获取“Album”对象的集合,并将其转换为相当简单的数据库连接:

// Get the albums var albums = await this.context.Albums .Where(x => x.Artist.ID == artist.ID) .ToListAsync(); 

这是创build的SQL:

 exec sp_executesql N'SELECT [Extent1].[ID] AS [ID], [Extent1].[URL] AS [URL], [Extent1].[ASIN] AS [ASIN], [Extent1].[Title] AS [Title], [Extent1].[ReleaseDate] AS [ReleaseDate], [Extent1].[AccurateDay] AS [AccurateDay], [Extent1].[AccurateMonth] AS [AccurateMonth], [Extent1].[Type] AS [Type], [Extent1].[Tracks] AS [Tracks], [Extent1].[MainCredits] AS [MainCredits], [Extent1].[SupportingCredits] AS [SupportingCredits], [Extent1].[Description] AS [Description], [Extent1].[Image] AS [Image], [Extent1].[HasImage] AS [HasImage], [Extent1].[Created] AS [Created], [Extent1].[Artist_ID] AS [Artist_ID] FROM [dbo].[Albums] AS [Extent1] WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134 

事实上,这不是一个大规模复杂的查询,但SQL服务器运行它需要将近6秒。 SQL Server Profiler报告它需要5742ms才能完成。

如果我将我的代码更改为:

 // Get the albums var albums = this.context.Albums .Where(x => x.Artist.ID == artist.ID) .ToList(); 

然后生成完全相同的SQL,但根据SQL Server Profiler,这只会在474毫秒内运行。

该数据库在“相册”表中有大约3500行,这并不是很多,并且在“Artist_ID”列上有一个索引,所以它应该相当快。

我知道,asynchronous有开销,但让事情慢十倍,似乎有点陡峭! 我在哪里错了?

我发现这个问题非常有趣,特别是因为我在Ado.Net和EF 6中使用async 。我希望有人解释这个问题,但是这并没有发生。 所以我试图在我身边重现这个问题。 我希望你们中的一些人会觉得这很有趣。

第一个好消息是:我转载了:)差别是巨大的。 有一个因素8 …

第一个结果

首先,我怀疑处理CommandBehavior事情,因为我读了一篇关于Ado async 的有趣文章 ,他说:

“由于非顺序访问模式必须存储整行数据,因此如果从服务器读取大型列(如varbinary(MAX),varchar(MAX),nvarchar(MAX)或XML )“。

我怀疑ToList()调用是CommandBehavior.SequentialAccess和asynchronous的是CommandBehavior.Default (非顺序,这可能会导致问题)。 所以我下载了EF6的源代码,并且在任何地方都放置了断点(当然在那里使用了CommandBehavior )。

结果: 没有 。 所有的调用是用CommandBehavior.Default ….所以我试图进入EF代码,以了解会发生什么…和.. ooouch …我从来没有看到这样的委托代码,一切似乎懒惰执行…

所以我试图做一些分析,以了解发生了什么…

我想我有些东西

下面是创build基准表的模型,其中包含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; } } 

这里是我用来创buildtesting数据和基准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() ),分析看起来是“正常的”,并且易于阅读:

ToList跟踪

在这里,我们find了秒表的8.4秒(分析速度减慢了性能)。 我们还发现呼叫path上的HitCount = 3500,与testing中的3500条线一致。 在TDSparsing器端,事情开始变得更糟,因为我们读取118 353对TryReadByteArray()方法的调用,这是发生缓冲循环。 (256kb的每个byte[]的平均33.8次调用)

对于async情况,真的是非常不同的….首先,调度.ToListAsync()调度ThreadPool,然后等待。 这里没什么了不起 但是,现在,这是ThreadPool上的async地狱:

ToListAsync地狱

首先,在第一种情况下,我们在整个呼叫path上只有3500个命中计数,在这里我们有118 371个。而且,你必须想象我没有放在屏幕上的所有同步呼叫…

其次,在第一种情况下,我们正在对TryReadByteArray()方法进行“118 353”调用,在这里我们有2 050 210个调用! 这是17倍…(在一个大1Mbarrays的testing中,是160倍以上)

另外还有:

  • 120 000创buildTask实例
  • 727 519 Interlocked电话
  • 290 569 Monitor电话
  • 98 283 ExecutionContext实例,包含264 481个捕获
  • 208 733 SpinLock电话

我的猜测是缓冲是以asynchronous的方式(而不是一个好的),并行的任务试图从TDS读取数据。 为了parsing二进制数据,创build了太多的任务。

作为一个初步的结论,我们可以说asynchronous是好的,EF6是伟大的,但在当前的实施EF6的async的使用增加了一个主要的开销,在性能方面,线程端和CPU端(12%的CPU使用率ToList()情况下, ToListAsync情况下20%的工作8至10倍…我运行它的旧i7 920)。

虽然做了一些testing,我还在想这篇文章 ,我注意到我想念的东西:

“对于.Net 4.5中的新asynchronous方法,它们的行为与同步方法完全相同,除了一个明显的例外:非顺序模式下的ReadAsync。

什么 ?!!!

所以我扩展我的基准,包括Ado.Net在常规/asynchronous调用,并与CommandBehavior.SequentialAccess / CommandBehavior.Default ,这是一个很大的惊喜! :

与ado

我们有与Ado.Net完全相同的行为! 捂脸……

我的确切结论是 :EF 6实现中有一个错误。 当对包含binary(max)列的表进行asynchronous调用时,它应该将CommandBehavior切换到SequentialAccess 。 在Ado.Net方面,创build太多任务的过程减慢了这个过程。 EF问题是它不使用Ado.Net,因为它应该。

现在你知道了,而不是使用EF6的asynchronous方法,你最好不得不以常规的非asynchronous方式调用EF,然后使用TaskCompletionSource<T>以asynchronous的方式返回结果。

注1:我编辑我的post,因为一个可耻的错误….我已经做了我的第一个testing通过networking,而不是在本地,有限的带宽扭曲了结果。 这里是更新的结果。

注2:我没有将我的testing扩展到其他用例(例如:具有大量数据的nvarchar(max) ),但也有可能发生相同的行为。

注3:通常用于ToList()情况是12%CPU(1/8的CPU = 1逻辑核心)。 不寻常的是ToListAsync()情况的最大值为20%,就好像调度程序不能使用所有的Treads一样。 这可能是由于太多的任务创build,或者可能是TDSparsing器的瓶颈,我不知道…