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

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

Использование async/await действительно увеличит пропускную способность сервера, но у асинхронной машины есть накладные расходы которые немного увеличат время выполнения операции. Проще говоря если у вас не несколько сотен запросов в секунду, быстрее будет работать синхронный код.

… а будет ли это критично, если у вас не несколько сотен запросов в секунду?

Синхронный код начнет проигрывать когда начнет истощаться пул потоков. Оценив будет ли это происходить в вашей среде исполнения можно выбирать ту или иную модель кода. Кроме того синхронный код немного чище.
НЛО прилетело и опубликовало эту надпись здесь

… а когда все ядра заняты?

НЛО прилетело и опубликовало эту надпись здесь
Для остальных — если все ядра занят делом, то значит цель балансировки достигнута.

Не в данном случае. В данном случае потоки заняты не "делом", а ожиданием — хотя могли бы заниматься делом.

НЛО прилетело и опубликовало эту надпись здесь
Поток это канал, контейнерного типа где размещаются процессы или часть кода.

Во-первых, нет.


Как любой канал он может простаивать, освобождаться.

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

НЛО прилетело и опубликовало эту надпись здесь
Во-первых, да. Это не только контейнер, но ещё и агрегатор.

Дайте, пожалуйста, ссылку на мейнстримное определение, подтверждающее вашу точку зрения.


Во вторых, асинхронность призвана равномерно загружать тики, а не освобождать.

Поясните?


Появление нового потока это как правило вынужденная мера, кода нельзя сдвинуть процесс по времени, не затрагивая смежных процессов

Так одновременное обслуживание клиентов веб-сервером (или прикладным сервером) — это оно и есть.

Не могли бы вы пояснить на примере про циклические алгоритмы?


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

НЛО прилетело и опубликовало эту надпись здесь

У-у-у, я понял в чем проблема.


Анимация — это особый случай. Во-первых, анимации обычно делаются путем периодических обновлений состояния по довольно простому закону, лишние абстракции тут только мешают (и, что еще хуже, уменьшают fps). Во-вторых, элементарные операции в анимациях — это зачастую CPU-bound операции.


Тем не менее, программа не может состоять только из анимаций. Попробуйте решить следующую задачу (можно и в уме): нужно загрузить данные, в процессе загрузки пользователю будет показываться анимированная заглушка. Как будет решаться такая задача? Источник данных — на ваш выбор, это может быть файл, удаленный сервер или что-то еще. Главное условие — анимация в заглушке никогда не должна замирать или двигаться "рывками".

Оценив будет ли это происходить в вашей среде исполнения можно выбирать ту или иную модель кода.

А потом, в случае роста, радостно переписывать
Чище за счет отсутствия слов async/await?
Это сугубо ваше дело как писать изначально. Чище за счет отсутствия слов async/await, необходимости оборачивать каждый результат в Task<> (ключевое слово var лучше не использую где возвращаемый тип не очевиден). Также пропадает необходимость писать ConfigureAwait(false) для каждой асинхронной задачи. Пропадает необходимость дописывать суффикс Async к названию метода как это требует MSDN

ConfigureAwait(false) для каждой задачи писать и так не обязательно.

Обязательно если вы пишете библиотеку

Если вы про взаимоблокировки — то есть более красивые способы их избежать. Если про что-то другое — то жду аргументов.

Какие, кстати? Я бы с удовольствием почитал.

  1. Task.Run(...)
  2. что-то вроде await ContextSwitcher.SwitchToBackground(); (нестандартное решение, но широко распространенное)
  3. просто не вызывать нигде Task.Wait или Task<>.Result

Первые два решения подходят для библиотек. Необходимость третьего можно прописать в документации на библиотеку.

Спасибо.


(третье мне точно не подходит, я людям не верю)

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


Да и то, на самом деле, не "обязательно", а желательно.

Если вы пишете библиотеку не только для себя то вы не можете знать где она будет работать. Если контекст синхронизации вам не нужен то необходимо его отпустить

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


Особенно смешно это "необходимо" выглядит в ситуации, когда после await вызывается код, который вы не контролируете (например, переданный делегат или вброшенный сервис).

Эту рекомендацию я взял из Best Practice и всегда ей придерживаюсь. Если вы вызываете код который вы не контролируете то очевидно контекст синхронизации вам нужен, просто восстановите его перед вызовом делегата. Но до тех пор пока контекст вам не нужен, зачем его дергать после каждого await?

Вот пример с делегатом:
public async Task DoWorkAsync(Action<byte[]> callback)
{
    byte[] content = await GetFileAsync();
    Thread.Sleep(3000); // подготовка данных для делегата
    callback(content);
}


Вот как я бы это сделал:
public async Task DoWorkAsync(Action<byte[]> callback)
{
    var context = SynchronizationContext.Current;
    if(context != null)
        await new NoContextYieldAwaitable();

    byte[] content = await GetFileAsync();
    Thread.Sleep(3000); // подготовка данных для делегата

    if (context != null)
        await context.RestoreAsync();

    callback(content);
}

Не вижу в вашем коде ConfigureAwait(false). Видимо, писать его и не настолько обязательно :-)

Вы зря придираетесь.
public async Task DoWorkAsync(Action<byte[]> callback)
{
    var context = SynchronizationContext.Current;
    byte[] content = await GetFileAsync().ConfigureAwait(false);
    Thread.Sleep(3000); // подготовка данных для делегата

    if (context != SynchronizationContext.Current)
        await context.RestoreAsync();

    callback(content);
}

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

Последнее я говорил
Если контекст синхронизации вам не нужен то необходимо его отпустить

Не столь важно как вы это сделаете, через ConfigureAwait или другим способом. Я лишь хочу показать на сколько это важно. К примеру возьму я библиотеку из nuget для WinForm и пока выполняется асинхронная функция она не должна прерывать UI без необходимости. Или еще хуже: может произойти дедлок если UI синхронно ждет завершение этого таска.
Я лишь хочу показать на сколько это важно.

Ну пока в качестве доказательства важности вы только ссылаетесь на Best Practices.


К примеру возьму я библиотеку из nuget для WinForm и пока выполняется асинхронная функция она не должна прерывать UI без необходимости.

Вообще-то, первое, что нужно сделать при вызове асинхронной функции из UI — это вызывать ее на контексте, отличном от UI-ного. Не надо надеяться, что функция внутри сделает это сама.

при вызове асинхронной функции из UI — это вызывать ее на контексте, отличном от UI-ного.

То-есть всегда писать так?
await Task.Run(() => httpClient.GetAsync(...));

Где я писал про "всегда"?

Если этот код внутри UI-обработчика, и вы хотите полностью отзывчивый UI, то да. Но зачем у вас обращение к HTTP-клиенту внутри UI-обработчика?

Кстати, я бы этот код написал чуть проще:


public async Task DoWorkAsync(Action<byte[]> callback)
{
    byte[] content = null;
    await Task.Run(async () => 
    {
        content  = await GetFileAsync();
        Thread.Sleep(3000); // подготовка данных для делегата
    });
    callback(content);
}
Эту рекомендацию я взял из Best Practice и всегда ей придерживаюсь.

Это все же рекомендация, а не обязательство.


Вот как я бы это сделал

… вот вы и усложнили код безо всякой на то необходимости.

Синхронный код начнет проигрывать когда начнет истощаться пул потоков.

Вопрос не в том, когда один будет проигрывать другому, а в том, когда потери будут заметны.


Кроме того синхронный код немного чище.

Это зависит от того, какой интерфейс у следующего слоя. Если уже асинхронный, то нет, не чище.

Недавно искал причину, как раз порождающуюю такую проблему.
Так что
Вопрос не в том, когда один будет проигрывать другому, а в том, когда потери будут заметны.
очень корректное высказывание.
По нашим тестам получилось что «цена» асинхронности может составлять до 30%, или давать выйгрыш в 10% относительно «эталонного» синхронного исполнения. -10% +30% времени.
Основная проблема — соотношение создания «новых» задач со временем ожидания результата от уже запущенных. В худшем сценарии просто выжирался пул потоков, а после плавно возвращались результаты, но долго :) Наиболее оптимальный сценарий оказался, если скорость создания задач была +- равна скорости получения результатов, тогда количество потоков равномерно.
Что в принципе и так написано в любой книжке: выравнивайте нагрузку
По нашим тестам получилось что «цена» асинхронности может составлять до 30%,

Я не очень понимаю, каким образом асинхронная машина может давать накладные расходы, пропорциональные времени запроса (а не количеству точек асинхронии).

А вот это другой очень и очень интересный вопрос. Есть пару подозрений, как освобожусь — проверю.
Там уже выходит за границу нашего приложения. а где-то «на границе» происходит что-то любопытное
Если микро сервис делает a + b, то это занимает 2-3 наносекунды. А создание\использование объекта Task занимает 500 микросекунд. Что больше чем три порядка.

И как это объясняет пропорциональные времени запроса накладные расходы? Должны бы быть пропорциональные числу объектов Task.

Должны те кто берет в долг. Никаких пропорций не существует. Если вы пишите сайт открытку, то можно использовать что угодно. Если у вас предпологается высокий нагруз. То использовать Таски нужно с умом, а не везде подряд, с поводом и без повода.
Если у вас предпологается высокий нагруз. То использовать Таски нужно с умом, а не везде подряд, с поводом и без повода.

Я где-то утверждал обратное?

Если синхронный код написан так, что бы не простаивать, то он не будет проигрывать. Асинхронный код имеет ряд недостатков. Накладные расходы на использование вполне приличное. Во вторых, сколько бы ни было у вас потоков в пуле, если у вас 4 ядра на сервере, они будут работать только как 4ре полноценных потока, выше головы не прыгнешь. Если у вас 4 ядра и 16 потоков. То каждый поток будет работать на 25% от номинальной производитености т.к. процессорное время будет распределено между всеми потоками.
Отличие асинхронности от многопоточности хорошо проилюстрюрованно. А вот абзац про параллелизм написан сумбурно и с ошибками.
Таки конкурентность != параллелизм, очень часто путаются эти термины.
[1, 2, 3, 4, 5]
И снова статья, где автор преувеличивает недостатки синхронной модели и скрывает недостатки асинхронной.

Во-первых, графики, они построены так, чтобы казалось: «Воу! Асинхронная модель выполняет больше задач!», а по факту, там просто какие-то кубики без привязки ко времени и размерам задачи. Классический маркетинговый прием.

Во-вторых, возьмем к примеру цитату:
Здесь мы можем видеть, что одна и та же задача скажем Т4, Т5, Т6 … обрабатывается несколькими потоками. Это красота этого сценария. Как мы можем видеть, что задача Т4 начала выполнение первой Потоком 1 и завершен Потоком 2. Подобным образом задча Т6 выполнена Потоком 2, Потоком 3 и Потоком 4. Это демонстрирует максимальное использование потоков.

Автор рассказывает, как же это круто, что 4 потока обрабатывают один запрос, но ни слова не говорит про синхронизацию этих 4х потоков, чтобы они могли работать с shared state задачи.
т.е. на графике, между каждым кубиком нужно добавить еще добавить прослойку из задержек на синхронизацию состояний.
Так же автор ничего не сказал про то, что состояния должны быть «сохранены» в неком объекте для continuation (продолжения выполнения через время).

Доля правды в статье есть, асинхронность – это хороший подход, но не на столько хороший, как его расхваливает автор.
Распишу пример в цифрах:
Задача занимает 1000 ед. времени.
Задача может быть разделена на 4 подзадачи по 250 ед. времени.
Синхронизация «железных потоков» 10 ед. времени.
Сохранение состояния континуации 1 ед. времени (допустим она сверхлегкая).
Есть 4 потока и 8 задач.

вариант1. Каждая подзадача зависит от предыдущей и не может быть выполнена в параллели.
(подготовить запрос в базу, сходить в базу, обработать результат, отдать наружу)
вариант2. Каждая подзадача независима и может быть выполнена в параллели.
(проксировать запрос на 4 других сервиса, и больше ничего не делать)
вариани3. Каждая 2 подзадачи независимы, 2 – зависимы.
(подготовить 2 запроса, отправить 2 запроса в 2 разных системы, смержить ответы и отдать наружу)

«Синхронная» многопоточная модель:
ход выполнения: 2 задачи + 2 синхронизации
8 задач = 1000 + 10 + 1000 + 10 = 2020 ед. времени (в каждом потоке).
1 задача будет выполнена за 1010 ед. времени.

«Асинхронная» многопоточная модель:
#1: Каждая подзадача зависит от предыдущей и не может быть выполнена в параллели.
ход выполнения: 8 подзадач + 8 сохранений состояния + 8 синхронизаций.
8 задач = (8 * 250) + (8 * 1) + (8 * 10) = 2088 е.д. времени.
1 задача будет выполнена за: (4 * 250) + (4 * 1) + (4 * 10) = 1044 ед. времени

#2: Каждая подзадача независима и может быть выполнена в параллели.
ход выполнения: 8 подзадач + 8 сохранений состояния + 8 синхронизаций.
8 задач = (8 * 250) + (8 * 1) + (8 * 10) = 2088 е.д. времени.
1 задача будет выполнена за: (1 * 250) + (1 * 1) + (1 * 10) = 261 ед. времени

#3: Каждая 2 подзадачи независимы, 2 – зависимы.
ход выполнения: 8 подзадач + 8 сохранений состояния + 8 синхронизаций.
8 задач = (8 * 250) + (8 * 1) + (8 * 10) = 2088 е.д. времени.
1 задача будет выполнена за: (3 * 250) + (3 * 1) + (3 * 10) = 788 ед. времени
*но тут все на много интереснее: этот вариант не может быть идеально положен на таймлайн потоков, потому 3-4 задачи из 8 будет раскиданы по таймлайну и выполнены в среднем за:
(5 * 250) + (5 * 1) + (5 * 10) = 1305 ед. времени, на ~25% дольше чем в синхронной модели.

Подводя итог:
— Синхронная модель = стабильность и предсказуемость по времени обработки и отдачи результата.
— Асинхронная модель = менее предсказуема в поведении и необходимо изучать среду в которой будет использована данная модель и требования к системе.

В идеальном варианте (#2) такой подход может дать в ~3.5 раза больше скорость обработки. Но таких задач на практике крайне мало, если есть вообще.
В более реальном случае (#1), такой подход может даже стабильно замедлить систему.
Или сделать ее нестабильной (#3) – когда все зависит от планировщика подзадач и часть задач может отдаваться на 25% быстрее, а часть на 25% медленнее, чем в синхронной модели.

ps: в примере #3, если нагрузка будет не сильно плотной, то система получит стабильный прирост на ~25%, если же нагрузка на систему возрастет, то система станет менее стабильной и может перейти даже в разряд деградации производительности. Потому, в этом случае, нужно делать замеры и подстраиваться под возрастающую нагрузку заблаговременно.

pps: данные примеры – это крайне упрощенная теория, а не реальные исследования. В реальном мире все на много сложнее и сильно может отличаться от приведенных цифр!

Вот поэтому и говорят, что асинхрония в первую очередь выгодна для IO-bound задач, а не вычислительных.

Да именно, и я прекрасно это понимаю.
Но к сожалению, очень многие этого не понимают. А подобные статьи лишь усиливают мнение, что Асинхронность – это идеальный подход без недостатков.

В статье явно прослеживается посыл: «синхронность – плохо, асинхронность – хорошо. точка.»
НЛО прилетело и опубликовало эту надпись здесь
Если физических ядер нехватает, не значит что не нужно создавать новые потоки когда это напрямую говорит разработчик.

Когда программисту нужен новый поток — он его создает через new Thread. И никто не ограничивает число потоков созданных этим способом.


Если же разработчик использует другие инструменты — то он не "непрямую говорит", а завязывается на детали реализации. Ну и кто ему злобный буратина при этом?

НЛО прилетело и опубликовало эту надпись здесь
«Другие инструменты» продвигаются компанией разработчиком фреймворка как «best practices».

Ну так в тех же best practices и объясняется, как эти инструменты работают.

на сетевые запросы тоже пул и вы по умолчанию зачем-то ограничены 2-мя соединениями на сайт.

Ну как "зачем-то"...


A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy.

HTTP/1.1

НЛО прилетело и опубликовало эту надпись здесь

Ожидания — вещь хрупкая и неожиданная, люди таких разных вещей ожидают или не ожидают… вот то, что в документации на видном месте этого нет — это правда печаль.


(В той же документации, кстати, неоднократно сказано "не надо создавать новые HttpClient, неполезно это".)

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

Публикации