Как правильно выполнить привязку к асинхронному вводу / выводу

#c# #asynchronous #async-await

#c# #асинхронный #асинхронное ожидание

Вопрос:

Я хочу полностью понять, как реализовать ключевые слова async и await в TAP. Я наткнулся на эту запись в MSDN: https://learn.microsoft.com/en-us/dotnet/csharp/async

В этом сообщении вы можете прочитать следующие два утверждения:

Для кода, связанного с вводом-выводом, вы ожидаете операции, которая возвращает задачу или задачу внутри асинхронного метода.

Вот два вопроса, которые вы должны задать, прежде чем писать какой-либо код:

Будет ли ваш код «ждать» чего-то, например, данных из базы данных?

Если ваш ответ «да», то ваша работа связана с вводом / выводом.

Будет ли ваш код выполнять дорогостоящие вычисления?

Если вы ответили «да», то ваша работа связана с процессором.

Если ваша работа связана с вводом / выводом, используйте async и ожидайте без Task.Run. Вы не должны использовать библиотеку параллельных задач. Причина этого подробно изложена в Async .

Связанная статья https://learn.microsoft.com/en-us/dotnet/standard/async-in-depth

Но я не понимаю, почему я не должен использовать Task.Запуск для извлечения данных из базы данных.

Я имею в виду, что для большинства методов я могу просто использовать часть счетчика асинхронности, такую как ExecuteNonQueryAsync или ExecuteScalarAsync

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

 public DataTable SelectData(string selectCommand)
{
    OpenConnection();
    DataTable data = new DataTable();
    Command.CommandType = CommandType.Text;
    Command.CommandText = selectCommand;

    using (MySqlDataAdapter adapter = new MySqlDataAdapter(Command))
    {
        adapter.Fill(data);
    }
    CloseConnection();
    return data;
}
  

Я бы предложил следующий метод для асинхронной передачи моих данных, но я не знаю, правильно ли это, согласно сообщению в MSDN.

 public async Task<DataTable> SelectDataAsync(string selectCommand)
{
    return await Task.Run(() => SelectData(selectCommand));
}
  

Кроме того, рассмотрим следующий метод:

 public void Query(string sqlCommand)
{
    OpenConnection();
    Command.CommandText = sqlCommand;
    Command.CommandType = CommandType.Text;
    Command.ExecuteNonQuery();
    CloseConnection();
}
  

Как мне написать этот метод, если я хочу поддерживать как асинхронный, так и синхронный? Я имею в виду, что я мог бы написать один и тот же код дважды, но вместо вызова синхронного метода я использую асинхронный.

 public async QueryAsync(string sqlCommand)
{
    OpenConnection();
    Command.CommandText = sqlCommand;
    Command.CommandType = CommandType.Text;
    await Command.ExecuteNonQueryAsync();
    CloseConnection();
}
  

Опять тот же сценарий. Я не должен использовать Task.Запуск для работы с привязкой ввода-вывода, но я не хочу писать почти один и тот же код дважды.

Кто-нибудь может уточнить, как это сделать правильно?

Комментарии:

1. Но я не понимаю, почему я должен использовать Task. Запуск для извлечения данных из базы данных. Вы должны использовать await и async для взаимодействия с базой данных, а не Task.Run , вы, вероятно, поняли это неправильно.

2. В вызывающем коде нет способа превратить что-то, что использует неасинхронную версию Query, в вашу функцию QueryAsync. Одним из вариантов уменьшения дублирования, которое вам необходимо, было бы сделать QueryAsync основной функцией и обернуть ее с помощью Task.Result, где вы используете Query.

3. @imsmn о, извините, я поставил should там вместо shouldn’t . Я отредактировал свой основной пост.

4. «Я бы предложил следующий метод для получения моих данных в асинхронном режиме» — этот метод на самом деле не будет получать ваши данные «в асинхронном режиме». Ваш текущий поток освобождается во время ожидания результата Task.Run , но вместо этого другой поток пула потоков thread ( Task.Run ) блокируется в ожидании SelectData завершения, поэтому в итоге ничего не достигается. Если вам нужны две версии — лучше оставьте только Async vesrion, потому что вызывающий абонент может ждать его результата синхронно, где это уместно, однако вы не можете получить асинхронную версию из sync one ( Task.Run не делает этого, как описано).

5. Взгляните на это: должен ли я предоставлять асинхронные оболочки для синхронных методов? Короткий ответ, нет. Также это: задача. Примеры выполнения этикета: не используйте Task.Run в реализации

Ответ №1:

MySqlDataAdapter имеет метод FillAsync. Вы могли бы заменить свой код на :

 public async Task<DataTable> SelectData(string selectCommand)
{
    using(var conn=new MySqlConnection(...))
    using( var cmd=new MySqlCommand(selectCommand,conn))
    using (MySqlDataAdapter adapter = new MySqlDataAdapter(cmd))
    {

        DataTable data = new DataTable()
        await adapter.FillAsync(data);
        return data;
    }
}
  

Я удалил методы OpenConnection() and CloseConnection() , потому что они гарантируют утечки соединений. Соединения должны быть недолговечными и закрытыми, даже если есть исключение. Создание их внутри using блока так, как показано во всех примерах, гарантирует, что они будут закрыты и удалены даже в тех случаях, когда a finally не будет вызван.

Строго типизированные результаты с помощью Dapper

Однако вместо использования DataTable со слабым типом, возможно, лучшим решением было бы использовать, например, Dapper и возвращать строго типизированную коллекцию, например :

 public async Task<List<Customers>> SelectData(string selectCommand)
{
    using(var conn=new MySqlConnection(...))
    {
        var results=await conn.QueryAsync<Customers>(selectCommand);
        return results.ToList();
    }
}
  

Использование Dapper упрощает использование параметризованных запросов, указывая параметры с помощью анонимных типов, например :

 var publishers = await connection.QueryAsync<Publisher>(
    "select * from Publishers where Name = @name", 
    new { Name = "O'Reilly" });
  

Попробуйте использовать это имя с конкатенацией строк….

Если вам действительно нужны динамические результаты, вы можете использовать dynamic вместо определенного типа.

Комментарии:

1. Я не беспокоюсь об открытии и закрытии соединений. Этим управляет мой класс, который содержит метод. Я должен был прояснить это в своем посте.

2. @MarvinKlein вам следует беспокоиться, потому что ваш код уже пропускает соединения. Есть причина, по которой люди не используют такие методы и глобальные объекты connection / command. Вы можете создать метод, который возвращает новый объект connection, но a CloseConnection() не требуется, когда Dispose() соединение уже закрыто

3. Мой класс реализует IDisposable и вызывает Command? .Dispose(); и соединение?. Dispose(); при уничтожении. Открытие и закрытие работают одинаково. если (команда?. Соединение?. Состояние!= ConnectionState. Открыть) { Команда?. Соединение?. Open(); }

4. @MarvinKlein что это предлагает, кроме расширения возможностей подключения, команды и считывателей, используемых адаптером данных? Это не скрывает сложность или инкапсулирует доступ к данным.

Ответ №2:

Но я не понимаю, почему я не должен использовать Task.Запуск для извлечения данных из базы данных.

Потому что это просто смещает проблему, а не решает ее; это заблокирует поток ThreadPool, а не текущий поток, ожидая ввода-вывода.

Если вы нацелены на настольное приложение, это может быть не серьезной проблемой, но это приведет к снижению масштабируемости в веб-приложении.

Как мне написать этот метод, если я хочу поддерживать как асинхронный, так и синхронный?

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

 private void CreateCommand(string sqlCommand)
{
    OpenConnection();
    Command.CommandText = sqlCommand;
    Command.CommandType = CommandType.Text;
}

public void Query(string sqlCommand)
{
    CreateCommand(sqlCommand)
    Command.ExecuteNonQuery();
    CloseConnection();
}

public async Task QueryAsync(string sqlCommand)
{
    CreateCommand(sqlCommand)
    await Command.ExecuteNonQueryAsync();
    CloseConnection();
}
  

Но как я должен использовать адаптер данных для извлечения данных.

Использование неблокирующего FillAsync :

 public async Task<DataTable> SelectData(string selectCommand)
{
    //...

    using (MySqlDataAdapter adapter = new MySqlDataAdapter(Command))
    {
        await adapter.FillAsync(data);
    }
    CloseConnection();
    return data;
}
  

Или вы могли бы использовать ExecuteReaderAsync в сочетании с DataTable.Load :

 public async Task<DataTable> SelectData(string selectCommand)
{
    //...

    data.Load(await Command.ExecuteReaderAsync());
    
    CloseConnection();
    return data;
}
  

Комментарии:

1. Привет, Джонатан, спасибо за ваш ответ. FillAsync поддерживается не каждым адаптером данных. Для MysqlDataAdapter, но не для FbDataAdapter для работы с базами данных Firebird. Я просто использовал MySqlDataApdater, потому что это больше известно большинству людей. Но я не знал, что MySQL также поддерживает этот метод. Знаете ли вы какое-либо другое решение, если FillAsync недоступен?

2. Вы могли бы использовать ExecuteReaderAsync в сочетании с DataTable.Load .