#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
.