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

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

Несколько созданных потоков ждут ответов. Код ничего не делает. После поступления ответов создаются новые потоки для отправки новых запросов. И этот процесс повторяется, пока не будут отправлены все запросы.

Непонятно. В js код, ожидающий ответа от сервера тоже "ничего не делает".

По сути, мой пример кода на Java (4910–1744)/4910=64% от общего времени не делает ничего, кроме как ждёт HTTP-откликов

А в js такого нет?

Что бы вы ни писали на JavaScript, преимущество очевидно — чем меньше клавиш мы нажимаете, тем меньше тратите времени и тем меньше вероятность внести баги. Однако так думают не все. Многие любят преобразовывать JavaScript в Java-подобный код под названием TypeScript

Ох, толсто то как. Используйте тогда однобуквенные переменные и в одну строку код пишите уж.

"Чем больше я могу получить информации при прочтении кода, тем меньше вероятность внести баг" - я поправил, не благодарите

Непонятно. В js код, ожидающий ответа от сервера тоже "ничего не делает".

Делает, асинхронная обработка выглядит так:

  1. Отправить 100 запросов сразу пачкой, не дожидаясь ответа

  2. Цикл

  3. Проверить нет ли готового ответа.

  4. Если есть, сделать его обработку

  5. Конец цикла

Таким образом свободное время тратится на обработку или проверку ответа. В модели с потоками же , поток посылает запрос, ждёт ответа, простаивает, затем обрабатывает. Таким образом, если у вас время ответа большое, то модель с потоками проигрывает по времени и наоборот

Спасибо за пояснение, но я всё ещё не понял.

Вот отправилось 100 запросов, ответа пока нет. Код ничего не делает (ну или проверяет, не пришло ли чего, но не уверен, что это можно рассматривать, как "что то делать").

Пришёл ответ - код снова что то делает.

В модели с потоками же , поток посылает запрос, ждёт ответа, простаивает, затем обрабатывает

Зачем код простаивает после ожидания ответа (или во время?) и перед обработкой?

Разве поток не может отправить другой запрос вместо простаивания?

Дополню, я не настоящий jav'ист, в java нельзя отправлять асинхронные запросы?

Вот отправилось 100 запросов, ответа пока нет. Код ничего не делает (ну или проверяет, не пришло ли чего, но не уверен, что это можно рассматривать, как "что то делать").

По факту вы будете дожидаться только, первого ответа, т.к время ответа у всех примерно одинаковое, а потом просто по очереди начнёте обрабатывать ответы в одном потоке.

С потоками время съедает переключение между ними. Дождались вы ответа от первого потока и казалось бы таже самая ситуация как в асинхронной, обрабатывай по порядку, но нет, начали вы обрабатывать ответ и на середине на кой то черт переключились на обработку ответа другого потока, время потратили на переключение.

Разве поток не может отправить другой запрос вместо простаивания?

Отправляет программист, а не поток. Все что вы можете попробовать сделать это сказать, "ну отправь по-братски" :)

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

Ну вот это отличается от изначального "Несколько созданных потоков ждут ответов. Код ничего не делает", т.к. складывается впечатление, что в js код в принципе ничего не ждёт, а всегда совершает полезную работу.

Отправляет программист, а не поток

Ну камон, Вы ж поняли о чём я
В целом спасибо за комментарии, стало понятно, что изначально пытался донести автор

Зачем код простаивает после ожидания ответа (или во время?) и перед обработкой?

Программист специально так написал код. Только простававет не код, а поток в котором код исполняется.


Если хотите поискать в этом практический смысл, то когда поток ждёт ответа, он гарантировано будет не занять, когда ответ придёт и сразу сможет обработать ответ. И поэтому время на обработку одного запроса будет чуть меньше. Если количество одновременно обрабатываемых запросов всегда меньше максимального количества потоков, то такой код будет работать чуть быстрее, чем код, который не ждёт ответа.


Разве поток не может отправить другой запрос вместо простаивания?

Может, но код надо писать по другому.


Дополню, я не настоящий jav'ист, в java нельзя отправлять асинхронные запросы?

Можно.

В JS у нас один поток отправляет все запросы по очереди, не дожидаясь ответа. После этого мы ожидаем ответ, но в это время может выполняться другая задача. Когда ответ пришёл, обработчик попадает в очередь и выполняется.
Если я правильно понял, то в Java на каждый запрос создаётся поток. Поток отправляет запрос и ждёт ответ. Количество потоков ограничено. Соответственно одновременно отправляются не все запросы, а (условно) THREAD_COUNT. Если это так то при количестве запросов не превышающем THREAD_COUNT время будет +- одинаковое.
Ну и если мы займем потоки не IO а чем-нибудь CPU-bound то у Java будет преимущество.

В Java вы вольны делать так, как хотите. Можете создать отдельный поток под обслуживание каждого нового соединения, если используется блокирующее API (IO), но можете возложить обслуживание соединений на один отдельный поток, если используете неблокирующий API (NIO, NIO2) с каналами и селекторами. На этих API напилено много библиотек. Я к тому, что в асинхронности JS нет ничего уникального, в конечном счете она также зиждится на нижестоящих механизмах ОС, как и Java. И если в Java вам дается свобода делать так, как вам нужно, будь то потоки или асинхронна модель, то в JS, по крайней мере в браузерном, с потоками будет посложнее.

Думаю задачи, под которые рождена технология четко определяют ее архитектуру. Java создана под перелопачивание данных, потому логично, что многопоточность в ее крови) JS рожден для реализации отзывчивого интерфейса пользователя в вебе, потому потоки ему особо не нужны, т.к. нечего обрабатывать. Сейчас, правда, многие пытаются натягивать сову на глобус... А крупные корпорации этому потворствуют... Но как же еще закрыть потребность в разработчиках? Если гора (будущий школьник-студент-еще-хз-кто, сокращенно "разраб" ) не идет к Магомету (всякие там faang, зеленые банки, и т. п. с их алгоритмами, плюсами, прерываниями и прочей неинтересной Горе ерундой), то приходится чем-то завлекать. Например: никаких там вам противных типов не надо, а числа пусть все будут Double, и синхронизацию эту проклятую тоже к черту с потоками. Аминь!

Вообще мотив исходной статьи какой-то реваншистский) Вечная попытка показать, что JS круче. ИМХО, если ваш инструмент подходит под ваши задачи, то используйте на здоровье)

Ну а "кровавый" энтерпрайз будет и дальше, ИМХО, разрабатываться на Java, т.к. Java для него создавалась.

>Если я правильно понял, то в Java на каждый запрос создаётся поток.
Только у автора в его примере. В общем случае — нет.

>Поток отправляет запрос и ждёт ответ. Количество потоков ограничено.
Количество потоков ограничено в основном доступными ресурсами. И на вполне обычной машинке с 48 ядрами и 64 гигабайтами памяти я создавал порядка 15 тыс потоков без особых проблем. Хотя скорее всего это не эффективно, и как правило есть варианты получше — но этот лимит далеко не на уровне сотен потоков уж точно.
Тут используется CompletableFuture.supplyAsync()
Он работает на CommonPool и никакие новые потоки не создает. Судя по разнице в скорости, на машине было порядка 8-16 потоков

Заголовок и сама статья вводят в заблуждение, создавая ощущение что в Java нельзя писать асинхронные запросы, а в js писать многопоточно плохо-плохо. И первое, и второе - не так.

Более того, сама задача достаточно вакуумная. Теперь добавьте реалистичности - попробуйте на каждой из загруженных страниц, не знаю, найти текст в h1 или ещё какую-то вычислительно затратную задачу, вдруг окажется что многопоточная модель начинает выигрывать. А то мы получили очень уж специфичную и подогнанную под нужный результат задачу.

Ну в чём-то она интересная - показывает что в некоторых случаях лучше использовать нативную асинхронность (например в C#), а не пытаться всё решать через потоки.

Нет, вы комментарии почитайте, они полезнее статьи. Там плохо написанный тест, производительность Явы показана в худшем свете (я не знаю, сделала бы она «ноду» в лучшем виде, но хотя бы отрыв был бы не такой большой), как если бы туда добавили Thread.Sleep, а потом развели бы руками.
Я предполагаю, что автор (оригинальной статьи) просто пиарится дешёвыми статьями, с примерами написанными на скорую руку. Погулил тут-там, и что-то состряпал.

Это все уже давно было обсуждено см



То как у Вас ни один ни другой вариант не оптимлен. Лучше и в том и в другом случае работать с пулом который будет запускать запросы не превышая при этом максимального заданного количества одновременно выполняемых запросов. nodejs по факту это сделал на своем системном уровне.


На основах эти есть фреймворк для java и не только https://en.wikipedia.org/wiki/Vert.x

Насколько я понял из статьи, Java так и делает: отправляет запрос только тогда, когда в пуле потоков есть свободные. То есть время ожидания примерно равно (количество страниц / размер пула) * (время загрузки страницы). А в js все запросы отправляются одновременно и время ожидания равно (время загрузки страницы)

Но треды все равно образуются новые. А в node вызывающий один тред. И никто так не делает в nodejs.

Образуются треды или нет зависит от того какой тредпул вы используете. Так есть и с фиксированным числом и с динамическом и даже есть тредпул с одним потоком как у node.

Не говоря уже о том, что это могут быть и не треды вовсе…

Ну тогда это уже не тред пулл а ExecutorService (могу в имени интерфейса ошибиться).

Так я ровно об этом — что в Java можно это все делать десятками способов, и обобщения выводов на Java, вообще ничего не значат.

>ExecutorService
Ну, скорее это будет Loom и «легкие» потоки.

У меня подозрение, что как раз наоборот: в js используется пул соединений и Connection: keep-alive. Поэтому запросы идут по конечному количеству долговременных соединений. В яве же каждый поток устанавливает новое соединение и проходит весь процесс SSL Handshake. Поэтому несмотря на то что получение данных параллельное сам процесс установки соединения требует времени. Но тут нужен профайлер и wireshark чтобы проверить оба предположения.

Есть другой вариант интерпретации результатов - вы померяли пинг + скорость вашего интернета к www.bbc.com. ;)

Прочитал статью и пролистывал комментарии, недоумевая, почему никто не указал на очевидную абсурдность бенчмарка.

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

Почему разница между Java и JavaScript почти трёхкратная?

Первое, что бросается в глаза -- это кондовый способ замера производительности:

        var start = System.currentTimeMillis();
        var contents = requestManyUrls(urls).get();
        var time = System.currentTimeMillis() - start;

и что самое основное -- отсутствие предварительного "прогрева". Это является причиной почему:

Если повторно запустить тот же код, общий размер будет близким, но не точно таким же. Предполагаю, что содержимое некоторых ссылок динамично.

То есть то, что аффтар тут намерял -- это работа http-клиента + класс лоадинг + динамическая оптимизация + создание новых статических инстансов -- т.е. сразу работу половины JVM.

Во-вторых, CompletableFuture.supplyAsync()использует дефолтный ForkJoinPool.commonPool(), поведение которого определяется внешними параметрами, железом и вендором Java-рантайма. В большинстве случаев размер дефолтного пула выбирается равеным количеству CPU core. То есть для 100 параллельных запросов из блокирующих операций использовалось всего 4-8 треда.

Далее:

Параллельные HTTP-запросы при помощи современного HttpClient

Формально работа HttpClient принципиально ничем не отличается от примера выше, за исключением того, что вместо ForkJoinPool.commonPool() используется java.util.concurrent.Executors.newCachedThreadPool(). В итоге то, что замерил аффтар -- это время выполнения запросов + время создания новых тредов для параллельной обработки. Поэтому если бы автор повторил тест сразу после первого прогона -- он был бы "сильно потрясен, весьма удивлен и крайне обескуражен".

Огромный код с современным HttpClient выглядит пугающе

                .map(url -> URI.create(url))
                .map(uri -> HttpRequest.newBuilder(uri))
                .map(reqBuilder -> reqBuilder.build())
                .map(request -> client.sendAsync(request, BodyHandlers.ofString()))

Ну да, особенно если искусственно навтыкать цепочку ненужных преобразований :)))

Почему разница между Java и JavaScript почти трёхкратная?

Потому что у аффтара изначально таковой была задача.

Код на JavaScript сначала выполняет один за другим 105 HTTP-запросов. Когда приходит ответ, движок JavaScript помещает в очередь задач небольшой обратный вызов. После получения всех ответов единственный поток по очереди обрабатывает их.

Ну то есть все рассчитано на то, что все 105 запросов и ответов должны обязательно поместиться в память, что никто не будет скачивать и закачивать гигабайтные запросы/ответы, что все данные для отправки уже доступны во время вызова, и что каждый из 105 обработчиков отработает моментально, чтобы никаки не задерживать других.

В Java это работает совершенно иначе. Создаётся множество потоков, каждый из которых отправляет один HTTP-запрос.

Не в Java, а конкретно в Java 11 HttpClient. В TCP стеке для отсылки требуется: открыть сокет, записать запрос и прочитать ответ. В классической синхронной модели все три операции блокирующие, и поэтому на каждый запрос требуется тред, который их последовательно обрабатывает, учитывая что открытие сокета немоментально, буфер данных для отсылки ограничен и может быть много меньше размера запроса, ответ также может приходить порциями, а обработка никак не должна препятствовать и интерферировать с другими запросами.

А вот в Java как раз есть способ реализовать низкоуровнево полную асинхронную реализацию, используя Selectors и Channels. Кроме того, есть прекрасный стек Netty в котором это уже реализовано, равно как и куча надстроек к нему, напр. полностью асинхронный https://github.com/timboudreau/netty-http-client Ну и напоследок стоит упомянуть, что начиная с версии 17 в JVM добавлен project Loom, с виртуальными тредами, которые позволяют использовать классическую синхронную модель, не заботясь о масштабировании, и которая "под капотом" выполняется асинхронно и неблокирующе. Только реальных тредов будет не один, а сколько душе угодно.

P.S. забыл упомянуть: www.bbc.com наверняка контролирует количество параллельных запросов с одного хоста, поэтому все тесты со 100 реквестами можно сразу выкинуть в корзину.

То есть то, что аффтар тут намерял — это работа http-клиента + класс лоадинг + динамическая оптимизация + создание новых статических инстансов — т.е. сразу работу половины JVM.

Есть мнение, что вся эта шелуха в данном случае занимает доли процента от всего затраченного времени, так что её можно смело отбросить.
Извините, я вас плюсанул, но попозже решил поподробнее поизучать код автора с помощью профайлинга кода на Яве и сетевых запросов как таковых. Попробую примазаться к вашей славе (хотя уже поздно).
Сравнение, к сожалению, двух реализаций очень плохое. Просто потому что, если посмотреть на запросы, которые делает, node-fetch и Ява, то мы увидим следующую разницу:
Java

Я приложил два скриншота, потому что соединения создаются разными способами, а значит заголовки разные.
Node

В ноде всё делается одной функцией, поэтому здесь никакой разницы нет.

Уже здесь видно, что сравнение нечестное. В случае с Явой мы скачиваем трафик неупакованный, а значит дольше. Вот разница между Явой и Нодой в этом случае (число запросов иногда разное, я не стал разбираться почему).
Разница в сетке

Вывод такой, что мы с помощью Явы скачиваем в 4 раза больше данных (40 мб).

Уже видите, что сравнение не честное.
К сожалению наскоком у меня не получилось сделать 100% правильный тест, просто из-за моей лени, поэтому я тупо добавил нужный мне заголовок и сравнил сетку ещё раз:
public CompletableFuture<List<UrlTxt>> requestManyUrls(List<String> urls) throws InterruptedException, ExecutionException {
    ...
    .map(HttpRequest::newBuilder)
    .map(r -> r.header("accept-encoding", "gzip,deflate,br")) // <-- сюда
    .map(HttpRequest.Builder::build)

И оказалось, что запросы практически выровнялись. Приведу лишь для Явы. Я не уверен, что мы посчитаем ссылки правильно таким образом, но я лишь хотел сравнить сетевую составляющую:
Java (gzip)

Здесь получилось чуть больше, чем на «ноде», потому что я первый запрос не «упаковывал», так как при распаковке пришлось бы работать с бинарными данными. Говорю, мой тест лишь поверхностный.

Как вы видите, мы скачали не 40 мб, но 11 мб, что гораздо лучше. Но, здесь есть одно «но». Можете открыть две картинки и увидите, что Ява тратить чуть больше времени на установку соединения.
Эту часть я особо не смог проанализировать. Запутался и забил. У меня сложилось такое впечатление, что мы тратим время на SSL handshake. Такое ощущение, что на каждое соединение мы заново устанавливаем подключение.
Вот, что делает основной поток:
Main Thread

Я так понимаю, что мы завязаны на SSL и на то, что первый запрос у нас «несжатый», поэтому пока он не выполнится, ничего не произойдёт.
Плюс, как видно, мы построчно копируем ответ из сервера через буфер в «стринг билдер» и тратим там тоже какое-то время.

Я попробовал этот код немного изменить (погуглил про NIO), немного стало получше, но опять не идеально.
public class HttpUtils {

    public static String get(String url) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI(url))
                .build();

        HttpResponse<String> response = HttpClient.newBuilder()
                .followRedirects(HttpClient.Redirect.ALWAYS)
                .build()
                .send(request, HttpResponse.BodyHandlers.ofString());

        return response.body();
    }

Ладно, не буду никого утомлять, просто вспомню чью-то фразу, что написать плохой бенчмарк легко, а хороший — сложно. Вот и все дела.
Извините, если пофлудил за ваш счёт.

Круто! У меня были подозрения, что весь процесс не достигает полного паралелизма выполнения либо по причине ограничения пулов, либо сам сайт не дает делать 100 запросов с одного хоста и ставит их в очередь. Интересно было бы посмотреть действительно ли все выполняется параллельно.

Насчет долгого SSL хендшейка есть предположение, что на сокете не выставлен TCP_NODELAY, однако я не нашел как его можно сконфигурировать в Java 9 HttpClient -- там настройки сильно ограничены.

Действительно выполняет запросы параллельно. Есть кое-какие запросы, которые сервер реджектит с 404. Плюс много ссылок ведут на разные домены типа «твитора», «фейсбука». Но если вы сомневаетесь с параллелизацией, то она есть.
Мне не понравились сами запросы, они, как бы вам сказать, немного подольше выполняются. Хотя и там, и там нормальный HTTP. А Ява даже обещает HTTP/2, присутствия которого я не особо заметил :)

Очень подозрительно много времени уходит на создание подключения (хоть в миллисекундах, но всё равно).

Потом я почитал, что есть разные библиотеки/API, где в сетевом подключении байты не копируются из нативных функций в приложение с помощью простого буфера в 8к (мы же выполняем тупую работу, послал запрос, получил ответ как строку), а действуют как-то иначе… Но как я сказал, я не стал эту тему слишком глубоко копать, может быть я бы переписал Ява код с некоторым пулом подключений и потоков.

И последнее, Ява да, создаёт много потоков, но в профайлинге я не видел того, что мы много времени на это тратим. Почти все потоки попросту простаивают, а трудятся 4-8 (у меня столько ядер) нативных тредов.

Я попытался ноду попрофилировать, но там JS кода не видно. Во всяком случае я его не нашёл :) всё упёрлось в нативный код с парсингом регулярки и кучи мелких вызовов имеющих отношение к Fetch.

Извините, если вышло сумбурно. Я много времени потратил, и не осилил fine tuning :)

В Java это работает совершенно иначе

Работает так, как хочет программист. Для достижения однопоточного поведения:

Executors.newSingleThreadExecutor().invokeAll(...)

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

Речь о том, что однопоточное поведение достигается ровно вот так. Одной строкой. То есть, утверждение автора, что «В Java это работает совершенно иначе» — просто чушь, и автор вероятно ничего об этом не знает. В Java это может работать кучей разных способов, начиная с того, что нет одного какого-то «штатного» клиента, который бы имел фиксированное поведение.

Если вы знаете, как реализовать более эффективные параллельные HTTP-запросы на Java, то напишите комментарий.

Netty - js даже близко не подберётся

Ну как бы да. Навскидку из имплементаций: https://github.com/AsyncHttpClient/async-http-client

Обсуждение потоков/асинхронного выполнения, когда bottleneck на уровне сети и опрашиваемой серверной части, вызывает много вопросов.

Если говорить про кастомные реализации, то у nodejs тоже есть переписанный вариант fetch (https://github.com/nodejs/undici), который к слову быстрее node-fetch реализованного поверх встроенного http клиента.

Это всё прекрасно, но в js нет возможности организовать отправку запросов в несколько потоков, поэтому в многопоточном процессоре js всегда будет уступать java

Скорее получение и обработку ответов. Отправка обычно не является проблемой вообще.

Получение - тоже. Но если обработка простая, то имеет смысл между отправкой и получением поделить побольше потоков. Например, на 16-поточном процессоре по 4 потока на отправку и на получение, 8 - на обработку. В js этого не достичь

>Получение — тоже.
Ну, чисто логически асинхронный код получения не такой простой, как синхронный код отправки запросов. А так-то для клиента и получение тоже не велика проблема, обычно.
НЛО прилетело и опубликовало эту надпись здесь
>наличия (общих для всех ЯП) граблей многопоточности
Ну-у-у. У некоторых ЯП (Java как раз из них) многопоточность была с рождения. А у некоторых ее наоборот, не было. Так что грабли-то есть у всех, а вот их виды могут быть очень даже разными. И способы обхода тоже.

В статье вроде бы говорится об однопоточном js, а не о ноде

В статье по сути описана разница между блокирующим и неблокирующим выполнением HTTP запроса - блокирующий подход ожидаемо показывает худший throughput, чем неблокирующий. При этом для JS показан неблокирующий HTTP вызов а для Java показан блокирующий вызов. При этом автор полностью проигнорировал тот факт, что Java позволяет работать в обоих парадигмах. Для того, чтобы использовать неблокирующие вызовы в Java, необходимо взять любой реактивный движек. Например Spring WebFlux.

Соответственно статья некорректна целиком и полностью. Некорректен заголовок статьи - сравнение блокирующих и неблокирующих HTTP вызовов почему то представлено как сравнение Java с JS. Некорректен текст статьи - автор показал блокирующие HTTP клиенты в Java, и полностью проигнорировал факт существования неблокирующих клиентов. И некорректен вывод, сделанный в статье - по сути разница между двумя шаблонами асинхронного взаимодействия представлена как разница между двумя языками.

И еще некорректно описана причина, по которой блокирующий подход показывает худший throughput, чем неблокирующий. Автор представил это как разницу между однопоточными и многопоточными приложениями. Но к многопоточности эти шаблоны проектирования не имеют никакого отношения. Асинхронный != Многопоточный.

PS.

И еще. Оценивая преимущества и недостатки блокирующих и неблокирующих HTTP вызовов автор принимает во внимание только одну метрику - throughput. Но в целом сетевое взаимодействие принято оценивать как минимум по двум метрикам - Latency и Throughput.

Обычно (но не всегда), блокирующие вызовы показывают меньший latency, чем неблокирующие. Т.е. при сравнении этих подходов необходимо принимать во внимание по какому именно критерию вы оптимизируете взаимодействие. Если вам необходим низкий latency, то скорее всего вам стоит взять блокирующий HTTP клиент. Если вам необходим высокий throughput - то неблокирующий клиент наверняка покажет лучшие результаты.

Если вам необходим низкий latency при высоком throughput, то вам необходимо либо скорректировать свои пожелания, либо попробовать решить эту проблему за счет горизонтального масштабирования, что выходит за рамки сравнения этих двух шаблонов.

Насколько я помню, nio - не о throughput, а о scalability. Ссылок на бенчмарки сейчас по памяти не приведу, но, как минимум, переключение контекстов должно добавлять накладных расходов. Да и не просто так однопоточный gc показывает более высокий именно throughput.

Насколько я помню, nio — не о throughput, а о scalability.

Ну тут именно о throughput.


Ссылок на бенчмарки сейчас по памяти не приведу, но, как минимум, переключение контекстов должно добавлять накладных расходов.

Если количество одновременных запросов больше количества потоков, то пропускная спосоность увеличивается, хотя каждый отдельный запрос может обрабатываться дольше. Как раз за счёт переключения контекстов в том числе.


Но вообще ожидание ответа или ожидание данных после установки соединения это очень долго. За это время можно успеть пару раз переключить контекст и сообщить другим соединениям, что они могут отправлять данные. И потом ещё какие-нибудь данные, полученные из третьего соединения обработать. А потом вернуться к первому соединению и проверить, пришло ли в него что-нибудь.


Да и не просто так однопоточный gc показывает более высокий именно throughput.

Не просто так, да. Потому что он специально останавливает всё, чтобы ничего не ждать, а просто перемолотить все объекты.

Насколько я помню, nio - не о throughput, а о scalability.

throughput (пропускная способность) - это грубо говоря число запросов/ответов в единицу времени.

latency, или round-trip time (круговая задержка) - это грубо говоря время получения ответа на запрос.

С определением метрики scalability я, к сожалению, не знаком. Поэтому не могу дать ответ на ваш комментарий.

Насколько я понимаю, все очень зависит от специфики. Много относительно мелких параллельных запросов? Используем nio (скорее всего). Одновременных запросов мало, но данных по каждому обработать дофига? Обычный io.

Пропускная способность — метрическая характеристика, показывающая соотношение предельного количества проходящих единиц (информации, предметов, объёма) в единицу времени через канал, систему, узел.

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

вообще-то блокирующий ввод-вывод самый быстрый и throughput у него максимальный. одна проблема: есть предел параллельно открытых соединений, но на современном железо это количество довольное большое. всё остальное про неблокирующий io это чисто маркетинг и разводилово

Боюсь, что здесь вы глубоко заблуждаетесь. В примитивном виде этот процесс можно представить так:

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

А при неблокирующем взаимодействии ваш поток шлет запрос, на запрос он получает фучу с ответом, на фучу вешает листенер, и сразу же шлет следующий запрос. Т.е. второй запрос отправляется не дожидаясь ответа на первый запрос. Соответственно throughput у нас получается больше.

throughput - это объём данных, которые можно прокачать через сокет. сами подумайте как быстрее будет: когда на мощной многоядерной(хотя бы) машине это будет делать 30 потоков или 1(4), которые кроме обработки запроса ещё и занимаются диспетчеризацией. + далеко не факт, что если у вас 4 ядра, то оптимально создать именно 4 потока а не 30. ввод вывод это не вычислительная задача.

при большом количестве соединений я думаю throughput сильно не снизится, но из-за перерасхода памяти, необходимости постоянно переключать контекст, система перестанет быть отзывчивой и начнёт расти latency, т.е. данные она та вам отправит на большой скорости, но перед этим подумает какое-то дополнительное время. Общий throughput будет снижаться.

все эти вещи тестировались и 10 и 15 лет назад ещё, на более слабом железе, чем сегодня есть дома у каждого, вердикт однозначный - throughput максимальный при блокирующем вводе-выводе, т.к. ваш поток занимается ровно тем, что от него требуется и не более. + количество потоков можно наращивать на лету до оптимальной величины, чтобы добиться полной загрузки железа.

неблокирующий ввод-вывод это другая концепция и для других условий, когда вы жертвуете пропускной способностью ради большего количества открытых соединений и соединения эти не требуют много от процессора. по этому вы используете концепцию, когда несколько потоков занимаются и диспетчеризацией соединений и отправкой данных и возможно ещё чем-то из бизнес логики. т.е. это другой тип задач, и aio это не серебренная пуля, просто на js было бы проблемно сделать нормальную многопоточность, вот зашли с другой стороны и добавили маркетинга, в то время как миллионы приложений работают на томкате и чувствуют себя отлично, а aio во фреймворки и сервера проникает довольно медленно

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

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

я вот не могу понять как 1 поток работающий на 2 сокета может быть быстрее двух потоков работающих на 2 сокета. это не противоречит вашему здравому смыслу?

Извините, но я не готов продолжать с вами дискуссию. Боюсь, что это совершенно бессмысленная трата времени.

ну на простой же вопрос вы ответить можете? потому что ну совсем смешно получается

Могу. Но мой ответ превратится в 2х часовую лекцию или серию статей. Писать эти статьи я не вижу никакого смысла, так как до меня их уже написали много раз. На лавры Шипилева я не готов претендовать :) Поищите его статьи и выступления.

так Шипилев никогда про ввод-вывод не рассказывал. Просто ваше утверждение, что 1 поток на 2 сокета справится с задачей пересылки быстрее, чем 2 потока на 2 сокета сильно интригующе и противоречит всему тому, что я читал 10 лет и моему здравому смыслу. Тезисно хотя б опишите, где возникают ограничения и проблемы, лекций не надо, и не забывайте что мы обсуждаем случай когда потоков десятки а не сотни. Если есть бенчмарки то вообще супер будет.

А про здравый смысл, я вам уже писал выше:

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

Большую часть времени (примерно 90%) ваш поток ждет ответа при блокирующем вводе/выводе. И чтобы добиться высокого throughput вам придется открывать много сокетов и много потоков. Сильно больше, чем доступных ядер. И тут как раз начнутся проблемы с переключением контекста.

И чтобы добиться высокого throughput вам придется открывать много сокетов и много потоков.

вывод не верен и он вообще не о том. ещё раз, преимущество блокирующего вывода в том, что вы не делаете лишней работы - не дёргаете постоянно системные вызовы, чтобы узнать состояние сокета, можно в него писать данные или нет, не разбираетесь кому какие данные за кого отправить, теряя на этом время, вы пишете ровно столько сколько надо, блокируетесь и вас будят ровно тогда когда надо (по прерыванию). ничего лишнего, вы грузите сокет на 100% ценой того, что диспетчеризацией занимается железо, а не код приложения как в aio. но есть "trade off" - хочешь throughput будь готов к накладным расходам, которые в какой-то момент начнут перекрывать выгоду. Вы же изначально это начали отвергать и по этому завзялся спор. К слову в этом треде ещё 2 человека явно написали, что я прав насчёт throughput блокирующего io. Просто нужно учитывать, что тысячи открытых соединений это многовато и скорее всего начнётся деградация общего throughput, которая в какой-то момент сравняется с aio, а потом станет совсем плохо, но скорее всего сотню соединений железо потянет без каких-либо тормозов, только что память начнёт кушать больше чем хотелось. Вопрос только в том, сколько конкретно нужно открыть соединений, чтобы throughput блокирующего io упал до уровня aio, два других вопроса это перерасход памяти и загрузка cpu в этот момент

Просто ваше утверждение, что 1 поток на 2 сокета справится с задачей пересылки быстрее, чем 2 потока на 2 сокета сильно интригующе и противоречит всему тому, что я читал 10 лет и моему здравому смыслу.

Если один поток обрабатывает 2 сокета, то на обработку одного секта уйдёт больше времени, чем если бы один поток работал с одним сокетом. Потому что какое-то время поток работает с другим сокетом. Это увеличивает latency.


Но суммарно на обработку двух сокетов уйдёт меньше времени, потому что пересылка данных для обоих сокетов происходит параллельно. Это увеличивает throughput.


Если мы сравниваем 1 поток обрабатывающий 2 сокета с двумя потоками, обрабатывающими 2 сокета, то latency у двух потоков будет чуть меньше, throughput будет чуть больше, но также вырастет количество используемого CPU и чуть чуть количество оперативной памяти.


Неблокирующие техники используются как раз для того, чтобы сильно сократить использование CPU и памяти при равном throughput с несущественным увеличением latency.

А про здравый смысл, я вам уже писал выше:

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

Большую часть времени (примерно 90%) ваш поток ждет ответа при блокирующем вводе/выводе. И чтобы добиться высокого throughput вам придется открывать много сокетов и много потоков. Сильно больше, чем доступных ядер. И тут как раз начнутся проблемы с переключением контекста.

Вы упускаете из виду тот факт, что протокол передачи и режим работы соединений фиксирован и зависит от решаемой задачи.


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


неблокирующий ввод-вывод это другая концепция и для других условий, когда вы жертвуете пропускной способностью ради большего количества открытых соединений

Если ваш сервер "умирает" от большого числа соединений — вместе с ним "умирает" и throughput.

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

А вот с latency наоборот - при блокирующем взаимодействии мы ответ на наш запрос получим раньше, чем при неблокирующем. С ростом throughput (количество запросов/ответов в единицу времени) у нас растет и latency (время, необходимое для получения ответа на запрос)

Поэтому термин "быстрый" весьма и весьма лукавый. Тут необходимо оперировать терминами throughput и latency

Хуже всего, что это пост в блоге компании :)

Комментарии намного интереснее самой статьи :)

таймвеб пылесосит авторов, молодцы, че, так можно кстати будет скупить весь хабр, через свой блог пускать. Дорого — но можно)

Ошибка статьи в том, что она сравнивает не языки, а 2 разных подхода к обработке задач - thread-per-task и poll. Причём poll вполне себе можно организовать и в Java. Причём даже более эффективно, чем это вообще возможно в js

И на самом деле даже не thread per task, так как в случае Java внизу использовалось два разных thread pool'а. Но автор, судя по всему, этого тоже не учитывает. Как в классическом "I have one udp joke but you won't get it"

Простите, мне кажется что основная ошибка статьи что автор не совсем понимает в чем разница между CPU-bound и IO-bound.

И действительно пока задача в том что надо "просто скачать и сохранить/агрегировать в памяти" оптимально решается через асинхронные интерфейсы на едином потоке исполнения.

Но как только мы попытаемся что-то сделать с результатом - тут все может очень сильно поменяться

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

А если модифицировать пример и добавить вычислительную задачу в каждый поток, то один поток в JS будет в разы медленнее.

Да тут уже выше Loom упоминали. Это по сути тоже самое примерно будет.

В таких публикациях главным вопросом всегда стоит "а что продаёт автор"..

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

Но если развёрнутое веб-приложение выполняет операции ввода-вывода, то многопоточность по большей мере теряет смысл, поскольку доступ к файловой системе — это узкое «бутылочное горлышко». Десять потоков не могут быть производительнее одного потока, вынужденного ждать содержимого от файловой системы.

А вы попробуйте подобное на собеседовании сказать :)

Да, в данных предложениях ошибки нет, но есть ошибка в архитектуре приложения, ибо для кого на просторах серверов валяются тонны книг и годы докладов по многопоточности?

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