【问题标题】:What happens with returning IEnumerable if used with async/await (streaming data from SQL Server with Dapper)?如果与 async/await(使用 Dapper 从 SQL Server 流式传输数据)一起使用,返回 IEnumerable 会发生什么情况?
【发布时间】:2019-08-27 10:24:41
【问题描述】:

我正在使用 Dapper 从 SQL Server 中的一个非常大的集合中流式传输数据。返回IEnumerable 和调用Query() 可以正常工作,但是当我切换到QueryAsync() 时,程序似乎尝试从SQL Server 读取所有数据而不是流式传输。

根据这个question,它应该可以很好地与buffered: false 一起工作,我正在这样做,但这个问题没有说明async/await

现在根据这个question,用QueryAsync()做我想做的事情并不简单。

我是否正确理解为async/await 切换上下文时迭代可枚举?

另一个问题是,当新的 C#8 异步流可用时,这是否可行?

【问题讨论】:

  • async / await 不会导致 IEnumerable 被迭代。尽管它可能会使某些迭代模式变得“棘手”。
  • async/await 与 QueryAsync 的行为方式无关。如果QueryAsync 的实现在返回 IEnumerable 之前读取了所有内容,则您无能为力。第二个问题与 Dapper 无关,因此不适用。无论 Dapper 如何工作,数据流都是创建处理管道的好方法
  • 对于异步流,除非将 QueryAsync 编码为返回 IAsyncEnumerable,否则它们不会产生任何影响
  • 投票结束的人应该得到......无论 Marc Gravel 决定什么。我怀疑他是必须实现异步流的人。
  • 好吧,我今天对另一个问题投了两票,没有任何解释,所以我真的认为人们想要关闭所有问题,如果这是一个更复杂的问题,或者不是一个可以运行示例代码的问题:) @ PanagiotisKanavos 我也喜欢这个密切的原因是“这个问题似乎与编程无关”。如果那与编程无关,我不知道是什么:)

标签: c# sql-server async-await dapper c#-8.0


【解决方案1】:

2020 年 3 月更新

.NET Core 3.0(和 3.1)现已发布,完全支持异步流。 Microsoft.Bcl.AsyncInterfaces 为 .NET Standard 2.0 和 .NET Framework 4.6.1+ 添加了对它们的支持,尽管出于理智的原因应该使用 4.7.2。作为.NET Standard implementation support explain上的文档

虽然 NuGet 认为 .NET Framework 4.6.1 支持 .NET Standard 1.5 到 2.0,但使用为 .NET Framework 4.6.1 项目中的这些版本构建的 .NET Standard 库存在几个问题。

对于需要使用此类库的 .NET Framework 项目,我们建议您将项目升级到面向 .NET Framework 4.7.2 或更高版本。

原答案

如果您check the source code,您会发现您的怀疑几乎是正确的。当buffered 为假时,QueryAsync同步流式传输。

if (command.Buffered)
{
    var buffer = new List<T>();
    var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
    while (await reader.ReadAsync(cancel).ConfigureAwait(false))
    {
        object val = func(reader);
        if (val == null || val is T)
        {
            buffer.Add((T)val);
        }
        else
        {
            buffer.Add((T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture));
        }
    }
    while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ }
    command.OnCompleted();
    return buffer;
}
else
{
    // can't use ReadAsync / cancellation; but this will have to do
    wasClosed = false; // don't close if handing back an open reader; rely on the command-behavior
    var deferred = ExecuteReaderSync<T>(reader, func, command.Parameters);
    reader = null; // to prevent it being disposed before the caller gets to see it
    return deferred;
}

正如评论所解释的,当预期返回类型为 IEnumerable 时,无法使用 ReadAsync。这就是为什么必须引入 C# 8 的异步枚举的原因。

ExecuteReaderSync 的代码是:

private static IEnumerable<T> ExecuteReaderSync<T>(IDataReader reader, Func<IDataReader, object> func, object parameters)
{
    using (reader)
    {
        while (reader.Read())
        {
            yield return (T)func(reader);
        }
        while (reader.NextResult()) { /* ignore subsequent result sets */ }
        (parameters as IParameterCallbacks)?.OnCompleted();
    }
}

它使用Read 而不是ReadAsync

C#8 异步流将允许重写它以返回 IAsyncEnumerable。仅仅更改语言版本并不能解决问题。

鉴于当前有关异步流的文档,这可能看起来像:

private static async IAsyncEnumerable<T> ExecuteReaderASync<T>(IDataReader reader, Func<IDataReader, object> func, object parameters)
{
    using (reader)
    {
        while (await reader.ReadAsync())
        {
            yield return (T)func(reader);
        }

        while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ }
         command.OnCompleted();
        (parameters as IParameterCallbacks)?.OnCompleted();
    }
}

Buuuuuut 异步流是只能在 .NET Core 上工作的东西之一,可能还没有实现。当我尝试在 Sharplab.io 中写一个时,Kaboom。 [connection lost, reconnecting…]

【讨论】:

  • 我明白了,所以枚举可枚举的不是异步,而是根本不可能用 async/await 来做 yield return,所以 buffered:false 实际上是 QueryAsync 的一个谎言: )
  • @IlyaChernomordik 不完全是。结果同步流式传输。 ExecuteReaderSync 是一个迭代器,它一到达就一次返回一个项目。该操作虽然同步运行
  • @IlyaChernomordik 您现在不需要等待整个可枚举项,但请求下一个项目将被阻止。只有在您明确要求时才会要求下一个项目。例如,在foreach 循环中,每次代码循环时都会请求它。问题是通过慢速连接返回一大行可能会阻塞Read() 相对较长的时间。
  • @IlyaChernomordik 在 C# 8 中您自己的代码必须更改为使用异步流,例如 await foreach(var item in dapperResults){..}。在这种情况下,每次尝试读取下一项时,迭代不会阻塞。
  • @IlyaChernomordik 如果你被缓冲,那么它将异步填充缓冲区,并且只返回(异步)给你满时;当您枚举它时,它是 OK 同步的,正是因为它已经全部在本地缓冲。问题场景是非缓冲的;当 非缓冲 它变得毛茸茸时 - 然后它是异步的,直到它获取数据,然后它切换到同步 同时仍从数据库读取数据
【解决方案2】:

在 dapper 的上下文中特别是,是的:它需要一个不同的 API,正如 @Panagiotis 的出色回答所解释的那样。以下不是答案,而是面临相同挑战的实施者可能希望考虑的额外背景。

我还没有为 dapper “添加”这个(虽然我 为 SE.Redis),我在各种选择之间左右为难:

  1. 为 .NET Core 添加新 API,返回适当的异步可枚举类型
  2. 将现有 API 彻底破坏为重大更改(“重大”等),将其更改为返回异步可枚举类型

我们可能会选择“1”,但我不得不说,第二个选项非常诱人,原因很充分:

  • 现有的 API 可能没有达到人们期望的效果
  • 我们希望新代码开始使用它

但奇怪的是IAsyncEnumerable&lt;T&gt; 的 .NET Core 3.0 特性 - 显然 Dapper 不仅仅针对 .NET Core 3.0;我们可以:

  1. 将该功能限制为.NET Core 3.0,并返回IAsyncEnumerable&lt;T&gt;
  2. 限制为.NET Core 3.0,并返回IAsyncEnumerable&lt;T&gt;
  3. 为之前的框架获取对 System.Linq.Async 的依赖(这不是“官方的”,但对于我们的目的来说已经足够官方了),并返回 IAsyncEnumerable&lt;T&gt;
  4. 返回一个自定义的可枚举类型,不是实际上IAsyncEnumerable&lt;T&gt;(但在可用时实现IAsyncEnumerable&lt;T&gt;),并手动实现状态机 - foreach 的鸭子类型性质意味着只要我们的自定义可枚举类型提供正确的方法,这将正常工作

我认为我们可能会选择选项 3,但重申一下:是的,有些事情需要改变。

【讨论】:

  • 我实际上发现,就我的目的而言,库所做的正是我所需要的:你得到一个同步流,但如果有意义的话,你会以异步方式得到它吗? :) 所以如果你们切换到 IAsyncEnumerable,我想与同步流相比会有很大的开销,因为它更复杂,所以可能可以实现两种不同的方法?特别是缓冲异步枚举没有意义(如果我什至理解正确的话)
  • @IlyaChernomordik 是的,当前的方法在缓冲的情况下工作正常,但非缓冲的情况并没有正确地尊重异步 - FWIW 我认为缓冲异步可枚举 确实 有意义 - 它甚至可能是为了简单起见。很可能我们只需要一个额外的IAsyncEnumerable&lt;T&gt; 方法来处理非缓冲的情况。我们讨论了在下一个专业中打破 API 以完全拆分缓冲/非缓冲,以便人们可以看到 List&lt;T&gt; - 或者可能是竞技场分配的 ReadOnlySequence&lt;T&gt;
  • @MarcGravell 频道在这里是一个不错的选择 5,尽管它们比 IAsyncEnumerable 有点“重”。
  • @PanagiotisKanavos 是的,我们使用 SE.Redis 进行 pub/sub 订阅的那条路线 - 但是...频道是在这里敲碎胡桃的气动锤;非常适合生产者/消费者场景,或具有多个读取器和/或多个写入器的场景,但是......对于这种情况? IAsyncEnumerable&lt;T&gt; 更适合
  • @IlyaChernomordik 确实,默认值是true(用于缓冲),因为大多数时候人们正在阅读 20 行等,而不是 200 万行——如果我们默认为 false,我们会导致人们看到如下错误:“我从方法中返回了可枚举,在途中传递了using 块”,“我枚举了它 7 次,但发生了不好的事情”,“它一直说我有一个开放的读者”。更容易默认为缓冲。
【解决方案3】:

(这应该是一个评论 // 到目前为止还没有足够的声誉)

Marc Gravell 在他的 reply 中提到,IAsyncEnumerable&lt;T&gt; 会更好,但由于依赖于 NET Core 3.0,因此依赖于 System.Linq.Async 可能会更好(这可以被视为“官方-够了”)...

在这种情况下,我想到了https://github.com/Dasync/AsyncEnumerable(MIT 许可证): 它旨在帮助

...(a)创建一个元素提供者,由于依赖于其他异步事件(例如等待句柄、网络流),生成一个元素可能需要很多时间,以及(b)一个处理这些事件的消费者元素一旦准备就绪,就不会阻塞线程(处理被安排在工作线程上)。

还有一个引用,RE:“当 C# 8.0 发布时会发生什么?” (FAQ)

C# 8.0 应该具有 Async Streams 的功能。当语言的版本最终发布时,它应该是您的应用程序的直接升级路径。

【讨论】:

  • 谢谢你,克里斯!
猜你喜欢
  • 2019-06-12
  • 2018-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-25
  • 2018-04-11
  • 1970-01-01
  • 2020-12-03
相关资源
最近更新 更多