Ответ 1
В системе без значительной нагрузки асинхронный вызов имеет несколько большие издержки. Хотя сама операция ввода-вывода является асинхронной независимо, блокировка может быть быстрее, чем переключение задач пула потоков.
Сколько накладных расходов? Давайте посмотрим на ваши временные числа. 30 мс для блокирующего вызова, 450 мс для асинхронного вызова. Размер пакета 32 КБ означает, что вам нужно около пятидесяти отдельных операций ввода-вывода. Это означает, что у нас примерно 8 мс служебных данных на каждый пакет, что очень хорошо соответствует вашим измерениям для пакетов разных размеров. Это не похоже на издержки только из-за асинхронности, хотя асинхронные версии должны выполнять гораздо больше работы, чем синхронные. Похоже, синхронная версия представляет собой (упрощенно) 1 запрос → 50 ответов, в то время как асинхронная версия заканчивается 1 запросом → 1 ответом → 1 запрос → 1 ответом ->..., оплачивая стоимость снова и снова снова.
Идем глубже. ExecuteReader
работает так же хорошо, как и ExecuteReaderAsync
. Следующей операцией является Read
а затем GetFieldValue
- и там происходит интересная вещь. Если какой-либо из них является асинхронным, вся операция выполняется медленно. Так что, безусловно, что-то совсем другое происходит, когда вы начинаете делать вещи действительно асинхронными - Read
будет быстрым, а затем асинхронный GetFieldValueAsync
будет медленным, или вы можете начать с медленного ReadAsync
, а затем GetFieldValue
и GetFieldValueAsync
будут быстрыми. Первое асинхронное чтение из потока является медленным, и медлительность полностью зависит от размера всей строки. Если я добавлю больше строк одинакового размера, чтение каждой строки займет столько же времени, как если бы у меня была только одна строка, так что очевидно, что данные все еще передаются поток за строкой - просто кажется, что они предпочитают читать все Строка сразу, как только вы начнете любое асинхронное чтение. Если я читаю первую строку асинхронно, а вторую синхронно - вторая читаемая строка снова будет быстрой.
Итак, мы видим, что проблема заключается в большом размере отдельной строки и/или столбца. Неважно, сколько у вас данных - асинхронное чтение миллиона маленьких строк выполняется так же быстро, как и синхронно. Но добавьте только одно поле, которое слишком велико, чтобы поместиться в одном пакете, и вы таинственно несете расходы на асинхронное чтение этих данных - как если бы каждому пакету требовался отдельный пакет запроса, и сервер не мог просто отправить все данные в один раз. Использование CommandBehavior.SequentialAccess
действительно повышает производительность, как и ожидалось, но по-прежнему существует огромный разрыв между синхронизацией и асинхронностью.
Лучшее выступление, которое я получил, было при правильном выполнении всего этого. Это означает использование CommandBehavior.SequentialAccess
, а также явную потоковую передачу данных:
using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
{
while (await reader.ReadAsync())
{
var data = await reader.GetTextReader(0).ReadToEndAsync();
}
}
При этом различие между синхронизацией и асинхронностью становится трудно измерить, и изменение размера пакета больше не влечет за собой смешные издержки, как раньше.
Если вам нужна высокая производительность в пограничных случаях, убедитесь, что вы используете лучшие инструменты, доступные в этом случае, - в этом случае потоковая ExecuteScalar
больших данных столбца, а не использование таких помощников, как ExecuteScalar
или GetFieldValue
.