C#异步/等待在读取DbDataReader上的效率(或滥用)

4

我是一位有用的助手,可以为您进行文本翻译。

我发现了一段经常使用的代码,起初似乎效率不高。(我知道优化有时可能是有害的,但我想知道)

介绍部分 - 相当简单的SP执行+读取返回的数据:

try
{
    await connection.OpenAsync();
    using (var command = connection.CreateCommand())
    {
        command.CommandText = sql.ToString();
        command.Parameters.AddRange(sqlParameters.ToArray());

        var reader = await command.ExecuteReaderAsync();
        if (reader.HasRows)
        {
            while (await reader.ReadAsync())
            {
                 var item = await GetProjectElement(reader);
                 list.Add(item);
            }
         }

         reader.Dispose();
     }      
}
finally
{
    connection.Close();
}

让我担心的是这个函数

等待获取项目元素(reader)

private async Task<Project> GetProjectElement(DbDataReader reader)
{
    var item = new Project
    {
        Id = await reader.GetFieldValueAsync<int>(1),
        ParentId = await reader.IsDBNullAsync(2) ? default(int?) : await reader.GetFieldValueAsync<int>(2),
        Name = await reader.IsDBNullAsync(3) ? default(string) : await reader.GetFieldValueAsync<string>(3),
        Description = await reader.IsDBNullAsync(4) ? default(string) : await reader.GetFieldValueAsync<string>(4),
        Address = await reader.IsDBNullAsync(5) ? default(string) : await reader.GetFieldValueAsync<string>(5),
        City = await reader.IsDBNullAsync(6) ? default(string) : await reader.GetFieldValueAsync<string>(6),
        PostalCode = await reader.IsDBNullAsync(7) ? default(string) : await reader.GetFieldValueAsync<string>(7),
        Type = (ProjectTypeEnum)(await reader.GetFieldValueAsync<byte>(8)),
        StartDate = await reader.IsDBNullAsync(9) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(9),
        EstimatedEndDate = await reader.IsDBNullAsync(10) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(10),
        ActualEndDate = await reader.IsDBNullAsync(11) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(11),
        WebsiteUrl = await reader.IsDBNullAsync(12) ? default(string) : await reader.GetFieldValueAsync<string>(12),
        Email = await reader.IsDBNullAsync(13) ? default(string) : await reader.GetFieldValueAsync<string>(13),
        PhoneNumber = await reader.IsDBNullAsync(14) ? default(string) : await reader.GetFieldValueAsync<string>(14),
        MobilePhoneNumber = await reader.IsDBNullAsync(15) ? default(string) : await reader.GetFieldValueAsync<string>(15),
        Key = await reader.IsDBNullAsync(16) ? default(Guid?) : await reader.GetFieldValueAsync<Guid>(16),
        OrganizationElementId = await reader.GetFieldValueAsync<int>(17),
        CompanyOrganizationElementId = await reader.IsDBNullAsync(18) ? default(int?) : await reader.GetFieldValueAsync<int>(18),
        IsArchived = await reader.GetFieldValueAsync<bool>(20),
        IsDeleted = await reader.GetFieldValueAsync<bool>(21),
        CreatedOn = await reader.GetFieldValueAsync<DateTime>(22),
        CreatedBy = await reader.GetFieldValueAsync<string>(23),
        ModifiedOn = await reader.IsDBNullAsync(24) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(24),
        ModifiedBy = await reader.IsDBNullAsync(25) ? default(string) : await reader.GetFieldValueAsync<string>(25)
    };

    return item;
}

正如您所看到的,有很多等待调用,编译器将其转换为状态机,是吗?

你可以在这里找到编译
由于未指定CommandBehavior,因此SP将以非顺序模式执行。 (可能的原因是在这种情况下表行不应该非常大,例如Project 链接)。
我的问题是:

1)这是否滥用了异步/等待,因为行数据已经缓存在内存中,对吗?

2)在这种情况下,Task<Project>是否纯粹是额外开销?

3)与不使用await相比,这种方法实际上会导致更差的性能吗?


最终思路: 如果我理解正确,我们将希望在内容可能超过合理长度的大型表行(例如存储varbinary(max)或blob)中使用CommandBehavior.SequentialAccess来异步读取它们。

如果你关心性能,那么最好的方法就是进行测试并找出答案。 - DavidG
@DavidG 我也想过这个问题,但是我很难确定如何在不同硬件执行的情况下正确测试它。我读到现代一些处理器对无条件跳转的成本非常小。然而,迄今为止我所学到的是最好不要跳转。 - noobed
为什么硬件是一个问题?一项比较性能测试应该有两个相同代码的变体,在同一台机器上运行。这样你就可以将它们进行比较,而不用担心哪个硬件在运行它。 - DavidG
3
“Plenty of GOTOs which means context switching over and over again.”这并不是上下文切换的意思,“gotos”(以及所有其他的async机制)不需要被JIT编译器转换为实际的跳转,即使涉及到跳转,当跳转方式相同时,分支预测器也能很好地处理这种情况。尽管如此,如果代码读取甚至单个列时都是“async”,你可能会测量到开销。无论在您的场景中是否显著,这是另一个问题。 - Jeroen Mostert
1个回答

6

正如其他人所指出的,GOTO语句不会导致上下文切换,并且速度非常快。

1)如果行数据已经在内存中缓冲了,那么这是否滥用了async/await?

ADO.NET允许实现者在实现基本类型时有很大的自由度。也许该行在内存中,也可能不在。

2)在这种情况下,Task是否是纯开销?

是的,如果操作实际上是同步的。这是您的ADO.NET提供程序的实现细节。

请注意,在这里状态机和await几乎没有任何开销;有一个async fast path,如果可能,代码将继续同步执行。

3)与不使用await的方法相比,这种方法是否实际上具有更差的性能?

可能不会。首先,性能影响不会由调用每个方法并继续同步执行所做的少量CPU工作驱动。你看到的任何性能影响都将是由在Gen0堆上抛出的额外Task<T>实例和必须进行垃圾回收造成的。这就是为什么现在有一个ValueTask<T>的原因。
但即使与您的数据库服务器的网络I/O调用相比,即使存在这样的性能影响,大多数情况下也很难察觉。话虽如此,如果您想了解微观性能惩罚,Zen of Async是一篇经典文章。

Channel 9的链接已经失效了(我也找不到微软的备份),但我想我在Youtube上找到了这个讲座:https://www.youtube.com/watch?v=zjLWWz2YnyQ。博客链接也失效了,但可以在archive.org上找到它[这里](https://web.archive.org/web/20190126143814/https://blogs.msdn.microsoft.com/lucian/2011/04/15/async-ctp-refresh-design-changes/)。 - jrh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接