Комментарии 59
… а будет ли это критично, если у вас не несколько сотен запросов в секунду?
… а когда все ядра заняты?
Для остальных — если все ядра занят делом, то значит цель балансировки достигнута.
Не в данном случае. В данном случае потоки заняты не "делом", а ожиданием — хотя могли бы заниматься делом.
Поток это канал, контейнерного типа где размещаются процессы или часть кода.
Во-первых, нет.
Как любой канал он может простаивать, освобождаться.
Во-вторых, вот как раз для того, чтобы он освобождался, а не простаивал, и применяется асинхронное программирование.
Во-первых, да. Это не только контейнер, но ещё и агрегатор.
Дайте, пожалуйста, ссылку на мейнстримное определение, подтверждающее вашу точку зрения.
Во вторых, асинхронность призвана равномерно загружать тики, а не освобождать.
Поясните?
Появление нового потока это как правило вынужденная мера, кода нельзя сдвинуть процесс по времени, не затрагивая смежных процессов
Так одновременное обслуживание клиентов веб-сервером (или прикладным сервером) — это оно и есть.
Не могли бы вы пояснить на примере про циклические алгоритмы?
Как, к примеру, будет выглядеть одновременная передача трех файлов в три сокета на одном потоке?
У-у-у, я понял в чем проблема.
Анимация — это особый случай. Во-первых, анимации обычно делаются путем периодических обновлений состояния по довольно простому закону, лишние абстракции тут только мешают (и, что еще хуже, уменьшают fps). Во-вторых, элементарные операции в анимациях — это зачастую CPU-bound операции.
Тем не менее, программа не может состоять только из анимаций. Попробуйте решить следующую задачу (можно и в уме): нужно загрузить данные, в процессе загрузки пользователю будет показываться анимированная заглушка. Как будет решаться такая задача? Источник данных — на ваш выбор, это может быть файл, удаленный сервер или что-то еще. Главное условие — анимация в заглушке никогда не должна замирать или двигаться "рывками".
Оценив будет ли это происходить в вашей среде исполнения можно выбирать ту или иную модель кода.
А потом, в случае роста, радостно переписывать
Чище за счет отсутствия слов async/await?
ConfigureAwait(false)
для каждой задачи писать и так не обязательно.
Обязательно если вы пишете библиотеку
Если вы про взаимоблокировки — то есть более красивые способы их избежать. Если про что-то другое — то жду аргументов.
Какие, кстати? Я бы с удовольствием почитал.
Task.Run(...)
- что-то вроде
await ContextSwitcher.SwitchToBackground();
(нестандартное решение, но широко распространенное) - просто не вызывать нигде
Task.Wait
илиTask<>.Result
Первые два решения подходят для библиотек. Необходимость третьего можно прописать в документации на библиотеку.
Нет, обязательно тогда, когда вы знаете, что кусок после await
можно не выполнять в том же синхронизационном контексте. Это не зависит от "библиотека-не библиотека".
Да и то, на самом деле, не "обязательно", а желательно.
Нет никакого "необходимо". Это правило хорошего тона по отношению к людям, которые будут использовать вашу библиотеку неправильно.
Особенно смешно это "необходимо" выглядит в ситуации, когда после 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(...));
Кстати, я бы этот код написал чуть проще:
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%,
Я не очень понимаю, каким образом асинхронная машина может давать накладные расходы, пропорциональные времени запроса (а не количеству точек асинхронии).
Там уже выходит за границу нашего приложения. а где-то «на границе» происходит что-то любопытное
И как это объясняет пропорциональные времени запроса накладные расходы? Должны бы быть пропорциональные числу объектов Task
.
Во-первых, графики, они построены так, чтобы казалось: «Воу! Асинхронная модель выполняет больше задач!», а по факту, там просто какие-то кубики без привязки ко времени и размерам задачи. Классический маркетинговый прием.
Во-вторых, возьмем к примеру цитату:
Здесь мы можем видеть, что одна и та же задача скажем Т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
. И никто не ограничивает число потоков созданных этим способом.
Если же разработчик использует другие инструменты — то он не "непрямую говорит", а завязывается на детали реализации. Ну и кто ему злобный буратина при этом?
Параллелизм против многопоточности против асинхронного программирования: разъяснение