简体   繁体   English

实体框架异步操作需要十倍的时间才能完成

[英]Entity Framework async operation takes ten times as long to complete

I've got an MVC site that's using Entity Framework 6 to handle the database, and I've been experimenting with changing it so that everything runs as async controllers and calls to the database are ran as their async counterparts (eg. ToListAsync() instead of ToList())我有一个使用 Entity Framework 6 来处理数据库的 MVC 站点,我一直在尝试更改它,以便所有内容都作为异步控制器运行,并且对数据库的调用作为它们的异步副本运行(例如 ToListAsync()而不是 ToList())

The problem I'm having is that simply changing my queries to async has caused them to be incredibly slow.我遇到的问题是简单地将我的查询更改为异步导致它们非常慢。

The following code gets a collection of "Album" objects from my data context and is translated to a fairly simple database join:以下代码从我的数据上下文中获取“Album”对象的集合,并转换为相当简单的数据库连接:

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

Here's the SQL that's created:这是创建的 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

As things go, it's not a massively complicated query, but it's taking almost 6 seconds for SQL server to run it.至于 go,这不是一个非常复杂的查询,但 SQL 服务器运行它几乎需要 6 秒。 SQL Server Profiler reports it as taking 5742ms to complete. SQL Server Profiler 报告它需要 5742 毫秒才能完成。

If I change my code to:如果我将代码更改为:

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

Then the exact same SQL is generated, yet this runs in just 474ms according to SQL Server Profiler.然后生成完全相同的 SQL,但根据 SQL Server Profiler,它的运行时间仅为 474 毫秒。

The database has around 3500 rows in the "Albums" table, which isn't really very many, and has an index on the "Artist_ID" column, so it should be pretty fast.该数据库在“Albums”表中有大约 3500 行,实际上并不是很多,并且在“Artist_ID”列上有一个索引,所以它应该非常快。

I know that async has overheads, but making things go ten times slower seems a bit steep to me?我知道异步有开销,但是让 go 慢十倍对我来说似乎有点陡峭? Where am I going wrong here?我哪里错了?

I found this question very interesting, especially since I'm using async everywhere with Ado.Net and EF 6. I was hoping someone to give an explanation for this question, but it doesn't happened.我发现这个问题非常有趣,特别是因为我在 Ado.Net 和 EF 6 中到处使用async 。我希望有人对这个问题做出解释,但它没有发生。 So I tried to reproduce this problem on my side.所以我试图在我这边重现这个问题。 I hope some of you will find this interesting.我希望你们中的一些人会觉得这很有趣。

First good news : I reproduced it :) And the difference is enormous.第一个好消息:我复制了它:) 差异是巨大的。 With a factor 8 ...系数为 8 ...

第一个结果

First I was suspecting something dealing with CommandBehavior , since I read an interesting article about async with Ado, saying this :首先我怀疑有什么处理CommandBehavior ,因为我读了一篇关于async与 Ado 的有趣文章,说:

"Since non-sequential access mode has to store the data for the entire row, it can cause issues if you are reading a large column from the server (such as varbinary(MAX), varchar(MAX), nvarchar(MAX) or XML)." “由于非顺序访问模式必须存储整行的数据,如果您从服务器读取大列(例如 varbinary(MAX)、varchar(MAX)、nvarchar(MAX) 或 XML),可能会导致问题)”

I was suspecting ToList() calls to be CommandBehavior.SequentialAccess and async ones to be CommandBehavior.Default (non-sequential, which can cause issues).我怀疑ToList()调用是CommandBehavior.SequentialAccess和异步调用是CommandBehavior.Default (非顺序的,这可能会导致问题)。 So I downloaded EF6's sources, and put breakpoints everywhere (where CommandBehavior where used, of course).所以我下载了 EF6 的源代码,并在各处放置了断点(当然,使用CommandBehavior地方)。

Result : nothing .结果:什么都没有 All the calls are made with CommandBehavior.Default .... So I tried to step into EF code to understand what happens... and.. ooouch... I never see such a delegating code, everything seems lazy executed...所有的调用都是使用CommandBehavior.Default .... 所以我试图进入 EF 代码以了解会发生什么......而且.. ooouch... 我从来没有看到过这样的委托代码,一切似乎都是懒惰的执行......

So I tried to do some profiling to understand what happens...所以我试着做一些分析来了解会发生什么......

And I think I have something...我想我有一些东西......

Here's the model to create the table I benchmarked, with 3500 lines inside of it, and 256 Kb random data in each varbinary(MAX) .这是创建我进行基准测试的表的模型,其中有 3500 行,每个varbinary(MAX) 256 Kb 随机数据。 (EF 6.1 - CodeFirst - CodePlex ) : (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; }
}

And here's the code I used to create the test data, and benchmark EF.这是我用来创建测试数据和基准 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);
    }
}

For the regular EF call ( .ToList() ), the profiling seems "normal" and is easy to read :对于常规 EF 调用( .ToList() ),分析似乎“正常”并且易于阅读:

ToList 跟踪

Here we find the 8.4 seconds we have with the Stopwatch (profiling slow downs the perfs).在这里,我们找到了秒表的 8.4 秒(分析会降低性能)。 We also find HitCount = 3500 along the call path, which is consistent with the 3500 lines in the test.我们在调用路径上也发现HitCount = 3500,与测试中的3500行一致。 On the TDS parser side, things start to became worse since we read 118 353 calls on TryReadByteArray() method, which is were the buffering loop occurs.在 TDS 解析器方面,情况开始变得更糟,因为我们读取了对TryReadByteArray()方法的 118 353 次调用,这是缓冲循环发生的原因。 (an average 33.8 calls for each byte[] of 256kb) (每个 256kb 的byte[]平均调用 33.8 次)

For the async case, it's really really different.... First, the .ToListAsync() call is scheduled on the ThreadPool, and then awaited.对于async情况,它真的很不一样......首先, .ToListAsync()调用被安排在 ThreadPool 上,然后等待。 Nothing amazing here.这里没什么了不起的。 But, now, here's the async hell on the ThreadPool :但是,现在,这是 ThreadPool 上的async地狱:

ToList异步地狱

First, in the first case we were having just 3500 hit counts along the full call path, here we have 118 371. Moreover, you have to imagine all the synchronization calls I didn't put on the screenshoot...首先,在第一种情况下,我们在整个调用路径上只有 3500 个命中计数,这里我们有 118 371 个。此外,您必须想象我没有放在屏幕截图中的所有同步调用......

Second, in the first case, we were having "just 118 353" calls to the TryReadByteArray() method, here we have 2 050 210 calls !其次,在第一种情况下,我们对TryReadByteArray()方法进行了“仅 118 353 次”调用,这里我们有 2 050 210 次调用! It's 17 times more... (on a test with large 1Mb array, it's 160 times more)它是 17 倍......(在使用大型 1Mb 阵列的测试中,它是 160 倍)

Moreover there are :此外还有:

  • 120 000 Task instances created创建了 120 000 个Task实例
  • 727 519 Interlocked calls 727 519 Interlocked呼叫
  • 290 569 Monitor calls 290 569 Monitor电话
  • 98 283 ExecutionContext instances, with 264 481 Captures 98 283 个ExecutionContext实例,有 264 481 个捕获
  • 208 733 SpinLock calls 208 733 SpinLock调用

My guess is the buffering is made in an async way (and not a good one), with parallel Tasks trying to read data from the TDS.我的猜测是缓冲是以异步方式进行的(不是一个好的方式),并行任务试图从 TDS 读取数据。 Too many Task are created just to parse the binary data.创建了太多任务只是为了解析二进制数据。

As a preliminary conclusion, we can say Async is great, EF6 is great, but EF6's usages of async in it's current implementation adds a major overhead, on the performance side, the Threading side, and the CPU side (12% CPU usage in the ToList() case and 20% in the ToListAsync case for a 8 to 10 times longer work... I run it on an old i7 920).作为初步结论,我们可以说 Async 很棒,EF6 很棒,但是 EF6 在其当前实现中对异步的使用增加了主要开销,在性能方面、线程方面和 CPU 方面(12% CPU 使用率在ToList()案例和 20% 在ToListAsync案例中,工作时间延长了 8 到 10 倍......我在旧的 i7 920 上运行它)。

While doings some tests, I was thinking about this article again and I notice something I miss :在做一些测试的时候,我又在思考这篇文章,我注意到我错过了一些东西:

"For the new asynchronous methods in .Net 4.5, their behavior is exactly the same as with the synchronous methods, except for one notable exception: ReadAsync in non-sequential mode." “对于 .Net 4.5 中的新异步方法,它们的行为与同步方法完全相同,除了一个值得注意的例外:非顺序模式下的 ReadAsync。”

What ?!!!什么 ?!!!

So I extend my benchmarks to include Ado.Net in regular / async call, and with CommandBehavior.SequentialAccess / CommandBehavior.Default , and here's a big surprise !所以我扩展了我的基准测试,将 Ado.Net 包含在常规/异步调用中,并使用CommandBehavior.SequentialAccess / CommandBehavior.Default ,这是一个很大的惊喜! :

废话

We have the exact same behavior with Ado.Net !!!我们与 Ado.Net 的行为完全相同!!! Facepalm...脸掌...

My definitive conclusion is : there's a bug in EF 6 implementation.我的最终结论是:EF 6 实现中有一个错误。 It should toggle the CommandBehavior to SequentialAccess when an async call is made over a table containing a binary(max) column.当对包含binary(max)列的表进行异步调用时,它应该将CommandBehavior切换为SequentialAccess The problem of creating too many Task, slowing down the process, is on the Ado.Net side.创建太多任务,减慢进程的问题是在 Ado.Net 方面。 The EF problem is that it doesn't use Ado.Net as it should. EF 的问题是它没有像它应该的那样使用 Ado.Net。

Now you know instead of using the EF6 async methods, you would better have to call EF in a regular non-async way, and then use a TaskCompletionSource<T> to return the result in an async way.现在您知道, TaskCompletionSource<T>使用TaskCompletionSource<T>异步方法,不如以常规的非异步方式调用 EF,然后使用TaskCompletionSource<T>以异步方式返回结果。

Note 1 : I edited my post because of a shameful error.... I've done my first test over the network, not locally, and the limited bandwidth have distorted the results.注 1:由于一个可耻的错误,我编辑了我的帖子......我已经通过网络而不是本地完成了我的第一次测试,并且有限的带宽扭曲了结果。 Here are the updated results.这是更新的结果。

Note 2 : I didn't extends my test to other uses cases (ex : nvarchar(max) with a lot of data), but there are chances the same behavior happens.注 2:我没有将我的测试扩展到其他用例(例如: nvarchar(max)与大量数据),但有可能发生相同的行为。

Note 3 : Something usual for the ToList() case, is the 12% CPU (1/8 of my CPU = 1 logical core).注 3:对于ToList()情况,通常使用 12% CPU(我的 CPU 的 1/8 = 1 个逻辑核心)。 Something unusual is the maximum 20% for the ToListAsync() case, as if the Scheduler could not use all the Treads.不寻常的是ToListAsync()情况下的最大 20%,好像调度程序无法使用所有 Treads。 It's probably due to the too many Task created, or maybe a bottleneck in TDS parser, I don't know...这可能是由于创建的任务太多,或者可能是 TDS 解析器的瓶颈,我不知道......

Because I got a link to this question a couple of days ago I decided to post a small update.因为几天前我得到了这个问题的链接,所以我决定发布一个小更新。 I was able to reproduce the results of the original answer using the, currently, newest version of EF (6.4.0) and .NET Framework 4.7.2.我能够使用当前最新版本的 EF (6.4.0) 和 .NET Framework 4.7.2 重现原始答案的结果。 Surprisingly this problem never got improved upon.令人惊讶的是,这个问题从未得到改善。

.NET Framework 4.7.2 | EF 6.4.0 (Values in ms. Average of 10 runs)

non async : 3016
async : 20415
ExecuteReaderAsync SequentialAccess : 2780
ExecuteReaderAsync Default : 21061
ExecuteReader SequentialAccess : 3467
ExecuteReader Default : 3074

This begged the question: Is there an improvement in dotnet core?这就引出了一个问题:dotnet 核心是否有改进?

I copied the code from the original answer to a new dotnet core 3.1.3 project and added EF Core 3.1.3.我将原始答案中的代码复制到一个新的 dotnet core 3.1.3 项目并添加了 EF Core 3.1.3。 The results are:结果是:

dotnet core 3.1.3 | EF Core 3.1.3 (Values in ms. Average of 10 runs)

non async : 2780
async : 6563
ExecuteReaderAsync SequentialAccess : 2593
ExecuteReaderAsync Default : 6679
ExecuteReader SequentialAccess : 2668
ExecuteReader Default : 2315

Surprisingly there's a lot of improvement.令人惊讶的是,有很多改进。 There's still seems some time lag because the threadpool gets called but it's about 3 times faster than the .NET Framework implementation.由于线程池被调用,似乎仍有一些时间滞后,但它比 .NET Framework 实现快 3 倍。

I hope this answer helps other people that get send this way in the future.我希望这个答案可以帮助将来以这种方式发送的其他人。

Still the same problem in .Net 5 and latest EF.在 .Net 5 和最新的 EF 中仍然存在同样的问题。 Seeing the same behaviour as mentioned above in a Blazor server project with EF.在带有 EF 的 Blazor 服务器项目中看到与上述相同的行为。 Switching to sync method brings down the time from approx 50 seconds to 1 second..切换到同步方法将时间从大约 50 秒缩短到 1 秒。

There is a solution that allows using async without sacrificing performance, tested with EF Core and MS SQL database.有一种解决方案允许在不牺牲性能的情况下使用异步,并使用 EF Core 和 MS SQL 数据库进行测试。

First you will need to make a wrapper for DBDataReader :首先,您需要为DBDataReader制作一个包装器:

  1. Its ReadAsync method should read the whole row, storing each column's value in a buffer.它的ReadAsync方法应该读取整行,将每列的值存储在缓冲区中。
  2. Its GetXyz methods should get the value from the aforementioned buffer.它的GetXyz方法应该从上述缓冲区中获取值。
  3. Optionally, use GetBytes + Encoding.GetString instead of GetString .或者,使用GetBytes + Encoding.GetString而不是GetString For my use cases (16KB text column per row), it resulted in significant speedup both for sync and async.对于我的用例(每行 16KB 文本列),它显着提高了同步和异步速度。
  4. Optionally, adjust the Packet Size of your connection string.或者,调整连接字符串的数据包大小。 For my use cases, a value of 32767 resulted in significant speedup both for sync and async.对于我的用例,值 32767 导致同步和异步的显着加速。

You can now make a DbCommandInterceptor , intercepting ReaderExecutingAsync to create a DBDataReader with sequential access, wrapped by the aforementioned wrapper.您现在可以创建一个DbCommandInterceptor ,拦截ReaderExecutingAsync以创建一个具有顺序访问的DBDataReader ,由上述包装器包装。

EF Core will try to access fields in a non-sequential manner - that is why the wrapper must read and buffer the whole row first. EF Core 将尝试以非顺序方式访问字段 - 这就是包装器必须首先读取和缓冲整行的原因。

Here is an example implementation (intercepts both async and sync):这是一个示例实现(拦截异步和同步):

/// <summary>
/// This interceptor optimizes a <see cref="Microsoft.EntityFrameworkCore.DbContext"/> for
/// accessing large columns (text, ntext, varchar(max) and nvarchar(max)). It enables the
/// <see cref="CommandBehavior.SequentialAccess"/> option and uses an optimized method
/// for converting large text columns into <see cref="string"/> objects.
/// </summary>
public class ExampleDbCommandInterceptor : DbCommandInterceptor
{
    public async override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
    {
        var behavior = CommandBehavior.SequentialAccess;

        var reader = await command.ExecuteReaderAsync(behavior, cancellationToken).ConfigureAwait(false);

        var wrapper = await DbDataReaderOptimizedWrapper.CreateAsync(reader, cancellationToken).ConfigureAwait(false);

        return InterceptionResult<DbDataReader>.SuppressWithResult(wrapper);
    }

    public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        var behavior = CommandBehavior.SequentialAccess;

        var reader = command.ExecuteReader(behavior);

        var wrapper = DbDataReaderOptimizedWrapper.Create(reader);

        return InterceptionResult<DbDataReader>.SuppressWithResult(wrapper);
    }

    /// <summary>
    /// This wrapper caches the values of accessed columns of each row, allowing non-sequential access
    /// even when <see cref="CommandBehavior.SequentialAccess"/> is specified. It enables using this option it with EF Core.
    /// In addition, it provides an optimized method for reading text, ntext, varchar(max) and nvarchar(max) columns.
    /// All in all, it speeds up database operations reading from large text columns.
    /// </summary>
    sealed class DbDataReaderOptimizedWrapper : DbDataReader
    {
        readonly DbDataReader reader;
        readonly DbColumn[] schema;

        readonly object[] cache;
        readonly Func<object>[] materializers;

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
        private T Get<T>(int ordinal)
        {
            if (cache[ordinal] != DBNull.Value) return (T)cache[ordinal];

            return (T)(object)null; // this line will throw an exception if T is not a reference type (class), otherwise it will return null
        }

        private DbDataReaderOptimizedWrapper(DbDataReader reader, IEnumerable<DbColumn> schema)
        {
            this.reader = reader;
            this.schema = schema.OrderBy(x => x.ColumnOrdinal).ToArray();

            cache = new object[this.schema.Length];


            byte[] stringGetterBuffer = null;

            string stringGetter(int i)
            {
                var dbColumn = this.schema[i];

                // Using GetBytes instead of GetString is much faster, but only works for text, ntext, varchar(max) and nvarchar(max)
                if (dbColumn.ColumnSize < int.MaxValue) return reader.GetString(i);

                if (stringGetterBuffer == null) stringGetterBuffer = new byte[32 * 1024];

                var totalRead = 0;

                while (true)
                {
                    var offset = totalRead;

                    totalRead += (int)reader.GetBytes(i, offset, stringGetterBuffer, offset, stringGetterBuffer.Length - offset);

                    if (totalRead < stringGetterBuffer.Length) break;

                    const int maxBufferSize = int.MaxValue / 2;

                    if (stringGetterBuffer.Length >= maxBufferSize)

                        throw new OutOfMemoryException($"{nameof(DbDataReaderOptimizedWrapper)}.{nameof(GetString)} cannot load column '{GetName(i)}' because it contains a string longer than {maxBufferSize} bytes.");

                    Array.Resize(ref stringGetterBuffer, 2 * stringGetterBuffer.Length);
                }

                var c = dbColumn.DataTypeName[0];

                var encoding = (c is 'N' or 'n') ? Encoding.Unicode : Encoding.ASCII;

                return encoding.GetString(stringGetterBuffer.AsSpan(0, totalRead));
            }

            var dict = new Dictionary<Type, Func<DbColumn, int, Func<object>>>
            {
                [typeof(bool)] = (column, index) => () => reader.GetBoolean(index),
                [typeof(byte)] = (column, index) => () => reader.GetByte(index),
                [typeof(char)] = (column, index) => () => reader.GetChar(index),

                [typeof(short)] = (column, index) => () => reader.GetInt16(index),
                [typeof(int)] = (column, index) => () => reader.GetInt32(index),
                [typeof(long)] = (column, index) => () => reader.GetInt64(index),

                [typeof(float)] = (column, index) => () => reader.GetFloat(index),
                [typeof(double)] = (column, index) => () => reader.GetDouble(index),
                [typeof(decimal)] = (column, index) => () => reader.GetDecimal(index),

                [typeof(DateTime)] = (column, index) => () => reader.GetDateTime(index),
                [typeof(Guid)] = (column, index) => () => reader.GetGuid(index),

                [typeof(string)] = (column, index) => () => stringGetter(index),
            };

            materializers = schema.Select((column, index) => dict[column.DataType](column, index)).ToArray();
        }

        public static DbDataReaderOptimizedWrapper Create(DbDataReader reader) 

            => new DbDataReaderOptimizedWrapper(reader, reader.GetColumnSchema());

        public static async ValueTask<DbDataReaderOptimizedWrapper> CreateAsync(DbDataReader reader, CancellationToken cancellationToken) 
            
            => new DbDataReaderOptimizedWrapper(reader, await reader.GetColumnSchemaAsync(cancellationToken).ConfigureAwait(false));

        protected override void Dispose(bool disposing) => reader.Dispose();

        public async override ValueTask DisposeAsync() => await reader.DisposeAsync().ConfigureAwait(false);


        public override object this[int ordinal] => Get<object>(ordinal);
        public override object this[string name] => Get<object>(GetOrdinal(name));

        public override int Depth => reader.Depth;

        public override int FieldCount => reader.FieldCount;

        public override bool HasRows => reader.HasRows;

        public override bool IsClosed => reader.IsClosed;

        public override int RecordsAffected => reader.RecordsAffected;

        public override int VisibleFieldCount => reader.VisibleFieldCount;


        public override bool GetBoolean(int ordinal) => Get<bool>(ordinal);

        public override byte GetByte(int ordinal) => Get<byte>(ordinal);

        public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) => throw new NotSupportedException();

        public override char GetChar(int ordinal) => Get<char>(ordinal);

        public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) => throw new NotSupportedException();

        public override string GetDataTypeName(int ordinal) => reader.GetDataTypeName(ordinal);

        public override DateTime GetDateTime(int ordinal) => Get<DateTime>(ordinal);

        public override decimal GetDecimal(int ordinal) => Get<decimal>(ordinal);

        public override double GetDouble(int ordinal) => Get<double>(ordinal);

        public override IEnumerator GetEnumerator() => reader.GetEnumerator();

        public override Type GetFieldType(int ordinal) => reader.GetFieldType(ordinal);

        public override float GetFloat(int ordinal) => Get<float>(ordinal);

        public override Guid GetGuid(int ordinal) => Get<Guid>(ordinal);

        public override short GetInt16(int ordinal) => Get<short>(ordinal);

        public override int GetInt32(int ordinal) => Get<int>(ordinal);

        public override long GetInt64(int ordinal) => Get<long>(ordinal);

        public override string GetName(int ordinal) => reader.GetName(ordinal);

        public override int GetOrdinal(string name) => reader.GetOrdinal(name);

        public override string GetString(int ordinal) => Get<string>(ordinal);

        public override object GetValue(int ordinal) => Get<object>(ordinal);

        public override int GetValues(object[] values)
        {
            var min = Math.Min(cache.Length, values.Length);

            Array.Copy(cache, values, min);

            return min;
        }

        public override bool IsDBNull(int ordinal) => Convert.IsDBNull(cache[ordinal]);

        public override bool NextResult() => reader.NextResult();

        public override bool Read()
        {
            Array.Clear(cache, 0, cache.Length);

            if (reader.Read())
            {
                for (int i = 0; i < cache.Length; ++i)
                {
                    if ((schema[i].AllowDBNull ?? true) && reader.IsDBNull(i)) 
                        
                        cache[i] = DBNull.Value;

                    else cache[i] = materializers[i]();
                }

                return true;
            }

            return false;
        }

        public override void Close() => reader.Close();

        public async override Task CloseAsync() => await reader.CloseAsync().ConfigureAwait(false);

        public override DataTable GetSchemaTable() => reader.GetSchemaTable();

        public async override Task<DataTable> GetSchemaTableAsync(CancellationToken cancellationToken = default) => await reader.GetSchemaTableAsync(cancellationToken).ConfigureAwait(false);

        public async override Task<ReadOnlyCollection<DbColumn>> GetColumnSchemaAsync(CancellationToken cancellationToken = default) => await reader.GetColumnSchemaAsync(cancellationToken).ConfigureAwait(false);

        public async override Task<bool> NextResultAsync(CancellationToken cancellationToken) => await reader.NextResultAsync(cancellationToken).ConfigureAwait(false);

        public async override Task<bool> ReadAsync(CancellationToken cancellationToken)
        {
            Array.Clear(cache, 0, cache.Length);

            if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                for (int i = 0; i < cache.Length; ++i)
                {
                    if ((schema[i].AllowDBNull ?? true) && await reader.IsDBNullAsync(i, cancellationToken).ConfigureAwait(false)) 
                        
                        cache[i] = DBNull.Value;

                    else cache[i] = materializers[i]();
                }

                return true;
            }

            return false;
        }
    }
}

I can't provide a benchmark right now, hopefully someone will do so in the comments.我现在无法提供基准,希望有人会在评论中提供。

Adding to the answer given by @rducom.添加到@rducom 给出的答案。 This issue is still present in Microsoft.EntityFrameworkCore 6.0.0 Microsoft.EntityFrameworkCore 6.0.0仍然存在此问题

The blocking part is actually SqlClient and the recommended workaround by @AndriySvyryd that works on the EF core project is:阻塞部分实际上是SqlClient ,@AndriySvyryd 推荐的适用于 EF 核心项目的解决方法是:

Don't use VARCHAR(MAX) or don't use async queries.不要使用 VARCHAR(MAX) 或不要使用异步查询。

This happened to me when reading a large JSON object and Image (binary) data with async queries.这发生在我使用async查询读取大型 JSON 对象和图像(二进制)数据时。

Links:链接:

https://github.com/dotnet/efcore/issues/18571#issuecomment-545992812 https://github.com/dotnet/efcore/issues/18571#issuecomment-545992812

https://github.com/dotnet/efcore/issues/18571 https://github.com/dotnet/efcore/issues/18571

https://github.com/dotnet/efcore/issues/885 https://github.com/dotnet/efcore/issues/885

https://github.com/dotnet/SqlClient/issues/245 https://github.com/dotnet/SqlClient/issues/245

https://github.com/dotnet/SqlClient/issues/593 https://github.com/dotnet/SqlClient/issues/593

The quickfix for me was wrapping the the call in a task and just use the synchronous method instead.对我来说,快速修复是将调用包装在一个任务中,而只使用同步方法。

It isn't a general fix it all solution, but at with small aggregates it can be confined to a small part of the application.它不是一个通用的解决方案,但对于小的聚合,它可以被限制在应用程序的一小部分。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM