Как стать автором
Обновить

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

Ваш друг — пакет AsyncFixer

Да, статические анализаторы — хорошая штука. Но инструментами нужно пользоваться с умом, понимая, почему они считают код ошибочным.
У вас случайно нет какого-то наглядного примера, когда AsyncFixer выдаёт некорректную рекомендацию?
Лучше использовать Task.FromResult() для оборачивания результата работы вендорных методов в Task. Это, конечно, не сделает код асинхронным, но мы хотя бы сэкономим на переключении контекста.

А в чём смысл оборачивать синхронный код в Task, если он не будет выполняться асинхронно? Не проще ли выполнять его синхронно?
Не просто лучше, а нужно выполнять синхронно. Но что если, например, нужно реализовать метод интерфейса, который возвращает Task?

оказывается, я всё делал не так ((

Я бы всё-таки не стал делать task elision по умолчанию, в основном из-за того что семантика выброса исключений меняется в зависимости от того, где именно в методе расположен выброс исключения; впрочем есть и другие причины.
У меня есть простое правило — использовать elision по умолчанию только для реализаций декораторов без валидации. В остальных случаях — смотреть на результаты регулярного аудита при помощи PerfView.
Я понимаю что в статье и не советуют использовать его всегда, но я неоднократно сталкивался с ситуацией, когда человек знакомится с концепцией task elision и начинает усиленно «изводить» await-ы =)
Да, тут больше речь о методах, которые пробрасывают таски дальше.

Отдельное спасибо за ссылку на Клэри :) Этот пост я как-то пропустил(
Ещё небольшая поправка: Stephen Cleary == Стивен Клири, а не Клэри.
Тоже думал над локализацией, но вот в этом видео он сам себя называет Стивен Клэри.

Очень хорошее замечание. На мой взгляд, лучше лишний раз не думать (и не заставлять коллег думать) и всегда писать async/await. Далеко не все даже опытные разработчики держат в голове все эти тонкости.

Добавил апдейт
У меня вот вопрос знающим людям. Теоретически, добавление
<add key="asp:UseTaskFriendlySynchronizationContext" value="true" />
должно пофиксить дедлок в ASP.Net, поскольку эта опция разрешает выполнение кода поле асинхронной операции в любом потоке из пула (и это реально так), но по факту, дедлок остается :(
Проблема не в номере потока, а в их количестве, поскольку ASP.NET запрещает выполнение одного запроса в двух потоках одновременно.
Звучит логично, спасибо за ответ! Но еще в статье сказанно, что

Исправляется это всё тем же ConfigureAwait(false).

Исходя из этого ограничения, ConfigureAwait не поправит ситуацию в общем случае.

Так если, например, заменить Task.Delay на

async Task MyDelay(int millisecondsDelay) => await Task.Delay(millisecondsDelay

то дедлок останется.
ConfigureAwait(false) вообще отвязывает операцию от контекста синхронизации и отправляет ее в пул потоков. SynchronizationContext.Current становится равным null.
После ConfigureAwait(false) снимаются все ограничения наложенные контекстом ранее.
Вот пример кода который все еще вызывает deadlock (WEB API 2):
[HttpGet]
public int Deadlock()
{
    StartWork().Wait();
    return 0;
}

private async Task StartWork()
{
    await MyDelay(100).ConfigureAwait(false);
    var s = "Just to illustrate the code following await";
}

private async Task MyDelay(int ms) => await Task.Delay(ms);

На месте MyDelay может быть любая другая библиотечная функция

Всё правильно. Здесь отвязывается от контекста Task, который создается в StartWork, но Task, возвращаемый Task.Delay() всё еще привязан к контексту. Если перенести ConfigureAwait(false) в MyDelay, то дедлока не будет.

Это да, просто хотел показать, что ConfigureAwait(false) не панацея, поскольку нет никакой гарантии, что вызываемый код внутри использует ConfigureAwait(false).

Справедливо. Поэтому Клэри и советует использовать ConfigureAwait (false) в библиотеках.

Почему же? После ConfigureAwait(false) ASP.NET больше не относит продолжение метода к старому запросу, это уже просто случайный поток из пула, про который ASP.NET вообще ничего не знает.

Это будет так только при определенных условиях (см. пример выше). Корме того, подобный хак может привести к ошибкам в рантайме, поскольку httpContext будет потерян и код "продолжения" не сможет получить доступ к данным запроса. Копирование http контекста из потока в поток это основная обязанность контекстов синхронизации применяемых в Asp.net, поэтому не стоит их выкидывать — лучше избавиться от этого антипаттерна .

Разобрался с aspnet:UseTaskFriendlySynchronizationContext. Эта опция включена по умолчанию. C помощью этой опции можно выбирать тип контекста синхронизации между AspNetSynchronizationContext и LegacyAspNetSynchronizationContext. В “обычном” (TaskFriendly) используется неблокирующая очередь для выполнения “продолжений”, а в “старом” “продолжения” вызывались “как есть” (из потока в котором завершилась асинхронная операция).

На дедлок эта опция не влияет так как в обоих случаях вызывается код, который переносит данные запроса из одного потока в другой.
НЛО прилетело и опубликовало эту надпись здесь
(1) У меня фото видно. Оно на habrastorage.
(2) Спасибо, интересно. Почитаю.
НЛО прилетело и опубликовало эту надпись здесь
Этот котик часть статьи) Картинка какбы говорит нам, что не нужно использовать Wait() без причины, иначе котику будет не хорошо)
НЛО прилетело и опубликовало эту надпись здесь
Не назван самый главный момент с async-ами: их вообще не надо использовать в 99% веб-приложений. Там нет таких задач, где они принесут заметную выгоду по производительности. А вот гемора, дедлоков, кучи лишних букв в коде, и головной боли — они приносят достаточно.

Взять хотя бы то, что у всех Entity Framework, его DbContext — не thread-safe, и в .net core нет SynchronizationContext-а. Т.е. EF вместе с async-ами — это полный капец, работать оно может только по счастливому стечению обстоятельств.

Я вообще считаю что добавление async-ов в C# — было ошибкой. Эдакой данью моде, заданной node.js — где костыль преподнесли за божью росу, дарующую перформанс. Тот 1% случаев, когда асинхронщина действительно что-то даст кроме гемора — можно было писать и обычными ContinueWith — и даже лучше было бы: меньше магии в таких сложных штуках — благо.
При чём тут счастливое стечение обстоятельств? Тот факт, что DbContext — не thread-safe, всего лишь означает что его нельзя использовать из разных потоков одновременно. Но это не означает что его нельзя использовать из разных потоков последовательно.

А await дает именно это: последовательное исполнение в разных потоках.

Что же до ошибок, костылей и дани моде — вы хоть раз пробовали выносить операции в фон в тех же вин-формах или WPF? Там без async никак.
Там последовательное исполнение заканчивается на любом коде джуна, которому надо Task вернуть имея 0, или идиота, начитавшегося статей автора, и втыкающего везде configureAwait(false) как мантру.

Еще раз — никто не играет в этот ваш кубик рубика, никому асинки нахер не надо, перестаньте их пиарить, вы наносите ущерб людям, вы ведете себя не этично с точки зрения инженерной этики.
И каким же образом этот ConfigureAwait(false) помешает потокобезопасности?

Кстати, у вас что-то не так с хронологией. Async/await пришел в Javascript из C#, а не наоборот.

Я предположу что товарищ имел в виду популяризованный node.js подход с (однопоточным) неблокирующим сервером и, как следствие, модой на end-to-end (от открытия сокета для приёма соединений до обращения к БД) асинхронный пайплайн.
Я хоть и вижу что это непопулярное мнение, но соглашусь с автором комментария как минимум частично.
Если запросы к БД работают быстро (<15мс) — оверхед от асинхронного API (и задержки из-за условно говоря IOCP и нагрузка на GC из-за временных объектов async state machine плюс самих Task<>) побъёт любой выигрыш (это не теория, это ежемесячный PerfView относительно нагруженного веб-проекта). Если же они работают медленно (>100мс) — на сервер никакой серьёзной нагрузки всё равно не создать, потому что обычно запросов требуется несколько, а сервисы, которые возвращают ответ на действие пользователя больше чем за 300мс должны гореть в огне.
Есть некоторый плюс в виде увеличения количества свободных потоков, но если это не микросервисное API, а что-нибудь потолще, то выгоды от «неблокирования» потока не так много (потому что основные расходы на существование потока это размер стека, а rooted object graph асинхронной операции может занимать сопоставимый со стеком объём памяти). В эпоху 32-битных процессов имело смысл экономить адресное пространство (и количество потоков), в современных 64-битных процессах смысла делать это существенно меньше.

Есть средства уменьшения оверхеда (и сама статья собственно про один из важных моментов), кроме того до нас рано или поздно доберутся ValueTask (правда он не так полезен как кажется), но сейчас ситуация обстоит примерно так как я описал выше — для динамической работы с БД без агрегирующих запросов и «широких» JOIN (а ни тем ни тем не место в условном хайлоаде в обработке запросов) асинхронные вызовы по умолчанию применять не стоит.
Конечно же можно пытаться параллелизовать и откладывать какие-то долгие операции внутри запроса так, чтобы скрыть, допустим, задержку внешних сервисов или какие-то операции, которые можно вынести «в фон», при этом их результаты не страшно потерять (если страшно то надо всё-таки message queue делать), но это всё-таки частные специфические случаи.

Отмечу что автор говорит про веб-приложения, про бэкенд и доступ к БД, так что аргументация про то что «в WPF без них никак» не кажется мне подходящей.

Написал три экрана текста на мобильном — 0 ответов, 0 аргументов, 2 минуса :)
Хоть описали бы с какой именно частью не согласны, что ли? Я правда хочу знать, что я не так думаю.

Вообще вы, конечно правы в том, что async — это не панацея, и переводить решение на async просто потому что так сейчас модно, скорее всего не стоит. Однако, если ASP.NET решение упирается в пул потоков стоит наряду с увеличением пула рассмотреть вариант, при котором на hot path все вызовы будут полностью асинхронными.

Кроме того, набирают популярность managed решения, в которых у юзеров нет доступа к размеру пула потоков.

Я бы смотрел на async/await в ASP.NET так: микрософт предложил нам легкий способ работы с асинхронным кодом, который позволяет малой кровью подготовиться к хайлоаду «из коробки». По началу, он был кривой и косой, но уже в ASP.NET Core с ним стало вполне приятно работать и почти все известные проблемы убрали.

Кстати, поделитесь опытом: как вы с помощью PerfView пришли к выводу, что при использовании async/await сильный оверхед?

И еще вопрос:
Если же они работают медленно (>100мс) — на сервер никакой серьёзной нагрузки всё равно не создать

Можете пояснить: почему не создать?
Про то что асинхронный код это лёгкий способ сделать «нормально по умолчанию» я совершенно согласен. Удобство налицо (я как вспомню «классический» асинк с APM и костыли чтобы это как-то склеить и надёжно обрабатывать ошибки, так зубы скрипят).
Более того, в .NET Core действительно всё стало ещё лучше (и я не столько про ASP.NET, сколько про стандартную библиотеку и рантайм).

В PerfView хорошо видно возрастающее количество выделений памяти. Кроме того, видно время CPU, проведённое в async state machine (Folding надо поставить в 0%), плюс переключения контекстов (я не про контексты потоков, а про то, что поток, на котором выполняется continuation, должен восстановить ExecutionContext CLR).
При профилировании PerfView рекомендую использовать свежую версию, и выключать галочку «TPL», с ней и большим количеством асинхронных операций, часто ETW начинает сильно тормозить (и даже увеличение in-memory буфера не помогает), и искажает результаты.

Про 100мс и нагрузку — я плохо выразился. Создать-то конечно можно любую нагрузку, вопрос в том, что происходит в эти 100мс. Это время проводится в каком-то другом сервисе, который скорее всего тоже имеет свои ограничения по масштабируемости, и узкое место скорее всего получится не в бэкенде, а в этом сервисе.

А как быть, если есть внешняя библиотека, имеющая только асинхронный интерфейс (и допустим там внутри запрос к микросервису на 10мс)? Получается если распространить async дальше по стеку вызовов, получим оверхед на ровном месте. Тогда лучше сразу делать GetAwaiter().GetResult() и жить дальше синхронно?

В общем случае, библиотеки, которые предоставляют _только_ асинхронный интерфейс вероятно не лучший вариант.
А так — я бы смотрел на профилирование.
Очень зависит от библиотеки. Например, коннектору к базе данных вполне стоит быть полностью асинхронным.
10 милисекунд — это где-то на три-четыре порядка больше, чем время, уходящее на создание асинхронщины. В этом случае можно вообще не волноваться.
Я собрал «на коленке» примитивный бенчмарк, который не согласен с этим утверждением. Вполне вероятно что в нём есть ошибка, но всё же думаю что на него стоит посмотреть.
Создание async state machine это не единственный источник оверхеда.
Да, что-то я погорячился. Вспомнил про примеры с наносекундами и решил, что оверхед не растет с ростом времени запроса.
Спасибо за советы. У нас сейчас как раз висит задачка на профилирование одно из сервисов. Попробуем сделать с PerfView.
Там нет таких задач, где они принесут заметную выгоду по производительности.

Там большая часть как раз проходит в IO — запросах к базе / редису / записи ответа. Отсутствие отдельных потоков ощутимо ускоряет код.


Эдакой данью моде, заданной node.js

Await в JS появился на пять лет позже.


Тот 1% случаев, когда асинхронщина действительно что-то даст кроме гемора — можно было писать и обычными ContinueWith

Напишите без await хотя бы простой цикл, который асинхронно что-то делает, а потом сравните сложность.


Уровня:


foreach(var itemKey in itemKeys)
{
     var fetched = await fetch(itemKey);
     var dbKey = await saveToDb(fetched);
     await updateCache(dbKey, itemKey, fetched);
}

Не уверен, что правильно понял идею комментария, но такой цикл — это довольно часто плохая идея. Сам цикл выполняется синхронно, каждая итерация будет ждать предыдущую. Не хочется придумывать пример, чтобы описать свои мысли лучше. Вот нагуглился http://gigi.nullneuron.net/gigilabs/avoid-await-in-foreach/


Правда c# 8 обещает убрать эту проблему.

Правда c# 8 обещает убрать эту проблему.
Как?
В связи с этим возникает вопрос — зачем вообще нужен C#, если можно писать синхронный многопоточный код на Го без потоков ОС, без извращений с async/await и calback hell-а с ContinueWith? Айда к нам, у нас есть много печенок
Как дженерики и нативную кодогенерацию в рантайме завезут, так сразу =)

Интересно наблюдать позицию по дженерикам в Go. Фазу «массивы и пустые интерфейсы решают все проблемы» уже прошли, фазу «используйте внешний препроцессор» тоже, теперь фаза «мы подумаем как сделать это в Go 2, но это ещё не точно»?
Дженерики важнее качественной многопоточности, кроскомпиляции и нативного рантайма? не смешите.

Вы наверное имели ввиду — кодогенерация и пустые интерфейсы. Вкупе они действительно решают.

Драфт про дженерики на контрактах в Го 2 был весьма жёстко раскритикован. В нём и правда неразрешимые противоречия. И судя по всему его отклонят.

Нет, я имел в виду то, что сказал. Нативная кодогенерация в рантайме.
«Качественная многопоточность» это CSP что ли? Ничего не мешает собрать его в free threading модели.
Кросскомпиляция это круто, правда, но только если нельзя ставить на целевую машину рантайм.

Основное преимущество Go это простота во всём. Это может быть важно, но мне не слишком :)

Вообще, если совсем коротко, то если вы понимаете async в C# — вам пора переставать писать C#. Вам надо оглядеться — вокруг неизведанные дали. От высоко оплачиваемого руководства джунами — которым с мудрости своей вы запретите использовать async, как и я. До прекрасных монад и аппликативных функторов на React и TypeScript. И горящих глазами мальчиков и девочек, понимающих спинным мозгом монады и аппликативные функторы, и делающих прекрасные аппы.

Советую совмещать.
Task это тоже вроде как монада, а async/await это просто синтаксический сахар. Вообще, работать с
монадами без специальных конструкций языка не особо удобно, так, например, в хаскеле почти всегда используется do нотация.

Кстати, ничто вам не мешает написать пару хелперов и работать с тасками через «from… in...» прямо как в Хаскеле :)
Как вы ловко перескочили с C# на React. Надеюсь, про то что вы руководите джунами вы слегка соврали, иначе не завидую я им.
Хотелось бы добавить на тему последнего пункта про «await в однострочном методе», что при возврате task'а напрямую (без использования async-await), вызов такого метода не будет виден в callstack'е при возниковении exception'а где-то в процессе работы «проброшенного» таким образом task'а. В некоторых случаях это может привести к серьёзным проблемам при расследовании подобных ошибок, особенно если в коде довольно много ветвлений, которые в конечном итоге заканчиваются вызовом одного и того же метода (например, вызова API).
Спасибо, добавил апдейт.
Кстати, для ASP.NET Core есть неплохой NuGet пакет Ben.BlockingDetector, который позволяет найти блокировки в асинхронном коде в приложении. Бывает полезно при отладке и поиске проблемных мест.
В соседней команде как раз недавно его впилили. Пока наблюдают)
Алекс Дэвис пишет, что затраты непосредственно на вызов асинхронного метода могут быть в десять раз больше затрат на вызов синхронного метода, так что тут есть ради чего стараться.

Как-то он сильно поскромничал. Там и 2, и 3 порядка разницы может быть.
Если посмотреть на любую статью о том, как устроен SynchronizationContext, в который должен свалиться асинхронный вызов, станет ясно почему так.
public Task MyMethodAsync()
{
    return Task.Delay(1000);
}

Я всё же делаю в таких местах await, т.к. накладные расходы тут незаметны, но зато есть правильный стэктрейс, если, вдруг, эксепшн возникнет.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий