C# асинхронная / ожидание эффективности (или злоупотребления) при чтении DbDataReader

Наткнулся на относительно часто используемый фрагмент кода, который поначалу казался неэффективным. (Я знаю, что оптимизация иногда бывает злой, но мне было интересно)

вводная часть - довольно простое выполнение 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();
}

Что меня беспокоит, так это функция

await GetProjectElement (читатель)

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;
}

как вы можете видеть, существует множество ожидающих вызовов, которые компилятор превращает в конечные автоматы, не так ли?

Вы можете найти упрощенную версию того, как сгенерированный компилятором код выглядит здесь. Множество GOTO, что означает переключение контекста снова и снова.

Поскольку SP был выполнен без указания CommandBehavior - данные будут в непоследовательном режиме. (вероятно, причина в том, что строка таблицы не должна быть очень большой в байтах для этого случая Project ссылка)


Мои вопросы:

1) это злоупотребление асинхронностью / ожиданием без очевидной причины, потому что данные строки уже буферизируются в памяти, верно?

2) есть Task<Project> чистые накладные расходы в этом сценарии?

3) будет ли этот подход иметь худшую производительность по сравнению с таким без awaitИНГ


И последнее: если я все сделаю правильно, мы бы хотели использовать CommandBehavior.SequentialAccess для больших строк таблицы, где содержимое может превышать разумную длину, что заставляет нас хотеть читать его асинхронно? (например, хранение varbinary(max) или BLOB-объектов)

1 ответ

Решение

Как уже отмечали другие, GOTO не вызывает переключение контекста и работает довольно быстро.

1) это злоупотребление асинхронностью / ожиданием без очевидной причины, потому что данные строки уже буферизируются в памяти, верно?

ADO.NET предоставляет разработчикам большую свободу действий в том, как именно они реализуют базовые типы. Возможно, строка в памяти, а может и нет.

2) Является ли Задача чистой накладной в этом сценарии?

Да, если операция на самом деле синхронная. Который является деталью реализации вашего провайдера ADO.NET.

Обратите внимание, что конечный автомат и await добавить практически никаких накладных расходов здесь; существует асинхронный быстрый путь, при котором код просто продолжает выполняться синхронно, если это возможно.

3) будет ли этот подход иметь худшую производительность по сравнению с одним без ожидания

Возможно нет. Во-первых, влияние на производительность не будет зависеть от того, какая небольшая работа ЦП выполняется для вызова каждого метода и продолжения синхронного выполнения. Любое влияние на производительность вы увидите из-за дополнительного Task<T> экземпляры, брошенные в кучу Gen0 и подлежащие сборке мусора. Вот почему сейчас ValueTask<T>,

Но даже это влияние на производительность, скорее всего, будет незаметным рядом с вызовом сетевого ввода-вывода на сервер базы данных. Тем не менее, если вы хотите знать штрафы микро-производительности, Zen of Async является классикой.

Другие вопросы по тегам