Comments 18

Отлично написано, очень много полезной информации, спасибо автор!

Мне вот интересно, как сочетаются следующие факты:


  • EntityFramework — не thread-safe
    • даже если сделать EF таковым, все равно нельзя на одном sql-connection, т. е. в одной транзакции, выполнять 2 запроса одновременно.
    • в asp.net core убрали synchronization context, который скидывал все в один поток

Прав ли я, что EF + async — работать не должны by design?


Прав ли я, что async не даст никакой пользы при <1000 запросов в секунду?


Т.е. прав ли я, что async-и в типичном приложении приносят риски отстрела конечностей, и при этом — не дадут никаких никаких реальных плюсов?

одном sql-connection, т. е. в одной транзакции, выполнять 2 запроса одновременно.
Замечу что Соединение и Транзакция не одно и тоже. Можно выполнять несколько запросов одновременно смотрим что такое Multiple Active Result Sets (MARS).
Я знаю про MARS, но это специфичная для MSSQL фича. В том же PostgreSQL, ее нет.
В firebird есть, в SQLite есть. В принципе, хороший дизайн — хендл на statement, и можно открывать их сколько угодно (в пределах лимита ресурсов сервера).
в asp.net core убрали synchronization context, который скидывал все в один поток

Не скидывал. Вы путаете с WinForms/WPF

Прав ли я, что EF + async — работать не должны by design?

Почему?

Прав ли я, что async не даст никакой пользы при <1000 запросов в секунду?

Только если у вас нет I/O-bound операций.

Т.е. прав ли я, что async-и в типичном приложении приносят риски отстрела конечностей, и при этом — не дадут никаких никаких реальных плюсов?

Какие риски вы имеете ввиду?
Асинхронность != параллельность.
await дает потоку, отправившему запрос в базу, вернуться в пул и начать выполнять что угодно другое. А потом какой-то поток, может даже тот же, вернется получить результаты запроса.
В один момент времени с контекстом как работал один поток, так и продолжает.
Теоретически — да. Практически, рассчитывать на то, что никто и никогда не поставит какой-нибудь Task.WaitAll в коде, кишашем async-ами — мне кажется дюже наивно.
А зачем что-то где-то вручную ставить? В 99% случаев все Async методы EF вызываются исключительно с await. А уж если программист сделал два подряд FirstOrDefaultAsync и не подождал первый await'ом, то он сам себе злобный буратино, что ж тут поделать.

Task.WaitAll может вообще понадобиться только в каких-то достаточно экзотических случаях, когда надо условно сделать запрос по сети и почитать из базы, и ты знаешь, что и то и другое долго, а запросы не зависят друг от друга и их можно сделать параллельно. Для чисто работы с базой код пишется так же, как если бы он писался синхронным, только всё возможные методы меняются на их *Async друзей, и перед ними ставится await. На этом все преобразования можно закончить, и никаких ошибок не будет. Программа просто сможет продолжать делать что-то абсолютно другое из другого запроса, а не зависнет всеми потоками в ожиданиях чтения из базы/по сети.
Могут и await забыть. И может сразу и не упадёт, а упадет в продакшне. Там же как повезёт — скинет в другой поток, или в том же выполнится. Я разгребал такого рода проблемы — там может быть всё очень нетривиально.

Ну и дальше берем какое-нибудь типичное веб-приложение: где 100 запросов в секунду от силы, производительность упирается в БД, а не в C#-код, и оптимизировать там надо совсем не скорость переключения потоков, а SQL-запросы. И берем лида, который не очень понимает в многопоточности вообще. Ну, например мне тут один загонял что без async/await-а ядра процессора будут простаивать (!!!) в ожидании завершения IO. Короче ему на хабре нашептали, что без async/await не модно, но при этом как это все работает и зачем оно нужно — он не понимает.

И вопрос — надо ли async/await среднестатистическим .NET-чикам — которые пилят ненагруженные приложения в стиле достань-положи JSON в БД. Или лучше сверху над этой темой написать большими красными буквами:
Если вы не хотите выжимать >1к RPS, и не разбираетесь досконально в теме — пишите как обычно, и не трогайте async-и. Tread Pool прекрасно работает, и отлично справляется со скедулингом операций и без async/await. Это вам не node.js — где всего один поток, и без них просто нельзя.

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

Асинхронное программирование — нереально удобная штука в правильных руках. И дело тут даже не в производительности, а в продуктивности — удобстве и скорости написания кода. Так-то да, при небольшой нагрузке можно обойтись и синхронными операциями, но асинхронные мне нравятся больше. Вот список преимуществ с моей точки зрения:


  1. Возможность управления задачами с помощью CancellationToken. В случае синхронных операций обычная практика — тупо закрыть сокет/поток/объект и перехватить ошибку. Для асинхронных операций же предусмотрен цивилизованный механизм остановки операции.


  2. Лёгкость взаимодействия с UI. Можно забыть про BeginInvoke и просто писать компактный код.


  3. Возможность масштабирования до тысяч запросов в секунду вообще без каких-либо усилий. Зачем переписывать код с синхронного на асинхронный, когда можно сразу писать асинхронный код, да ещё с меньшими трудозатратами?


Прав ли я, что EF + async — работать не должны by design?

Выдается ошибка выполнения при попытке в том же DB context выполнить второй async не дожидаясь await первого, если я правильно понял о чем идет речь.

Я как-то давно написал собственный кросс-платформенный однопоточный планировщик для эффективной работы с сетью, использующий IOCP и epoll — в те времена велосипед был необходимостью. По сравнению с универсальным штатным планировщиком, IOPS моего решения оказался почти на порядок выше.


Основной причиной повышения производительности стал отказ от выполнения задач на пуле потоков, так как раньше львиная доля ресурсов расходовалась на переключение контекста и межпотоковую синхронизацию, а теперь в режиме кооперативной многозадачности с ручным переключением это оказалось не нужно. При этом возможность отправить CPU-bound задачи на пул потоков сохранилась.


Недостатком же оказалась невозможность использования моего планировщика в библиотеках из-за жёсткой зависимости от конкретных реализаций, например, использования системного Socket вместо универсального Stream и того самого ConfigureAwait(false);, используемого не по делу. В итоге библиотечный код просто работал в планировщике по умолчанию. Как по мне, так не вызываемый код должен решать, в каком контексте он будет работать, а вызывающий.

Я привык явно всегда и везде вызывать ConfigureAwait(), даже когда это не нужно т.е. ConfigureAwait( true ), на самом деле это стандартный code style для Resharper и просто не поставить не получится, будет все перечеркнуто warnings, а если в jenkins эти warnings подняты до уровня error, то вообще билд не будет делаться (у нас так строго, да)
С точки зрения программирования это тоже полезно, потому что заставляет разработчика указать explicit intention, хочет или нет он, чтобы сохранялся контекст для выполнения continuation на нем.
на самом деле это стандартный code style для Resharper и просто не поставить не получится, будет все перечеркнуто warnings, а если в jenkins эти warnings подняты до уровня error, то вообще билд не будет делаться (у нас так строго, да)

Идиотизм, если честно.

Откройте для себя Fody с плагином ConfigureAwait, который делает всё это автоматом в готовой сборке, без мусора в коде. В чём смысл заниматься обезьяньим трудом, да ещё и заставлять заниматься обезьяньим трудом себя и других с помощью инструментов?

Only those users with full accounts are able to leave comments. Log in, please.