Асинхронное выполнение на Java и JavaScript


При необходимости в JavaScript можно запускать дополнительные потоки. Но обычно в Node.js или в браузерах весь код на JavaScript выполняется в одном потоке. В браузерах один и тот же поток рендерит содержимое веб-страницы на экран. По сути, один поток выполнения занимается всеми задачами, потому что приложения JavaScript пользуются преимуществами асинхронного выполнения. Для асинхронного выполнения задача помещается в очередь задач. Задачи из очереди одна за другой выполняются единственным потоком. Например, вторая строка кода выполняет планирование асинхронной задачи, которая запускается после завершения текущей задачи:

console.log("1");
setTimeout(()=>console.log("2"));
console.log("3");

Результатом работы кода будет 1 3 2.

В Java API под асинхронным выполнением обычно подразумевается, что задача выполняется в новом выделенном потоке. Например, представленный ниже код при помощи метода supplyAsync() планирует асинхронную задачу:

System.out.println("current thread: " + Thread.currentThread().getName());
var future = CompletableFuture.supplyAsync(() -> Thread.currentThread().getName());
System.out.println("current thread: " + Thread.currentThread().getName());
System.out.println("task thread: " + future.get());

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

current thread: main
current thread: main
task thread: ForkJoinPool.commonPool-worker-1

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

Сравнение производительности многопоточных и однопоточных приложений


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

Когда многопоточная Java работает медленнее, чем однопоточный JavaScript


Давайте попробуем скачать содержимое примерно ста случайных URL. При этом воспользуемся возможностью и сравним производительность древнего HttpURLConnection и современного HttpClient.

Представленный ниже код извлекает все абсолютные ссылки с https://www.bbc.com/news/world (около 100 URL), загружает их содержимое, а затем выводит общее время, потраченное на параллельное получение содержимого:

public abstract class Runner {

    abstract CompletableFuture<List<UrlTxt>> requestManyUrls(List<String> urls) throws Exception;

    void run() throws Exception {
        var urls = getUrlsFromUrl("https://www.bbc.com/news/world");

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

        var totalLength = contents.stream()
                .mapToInt(o -> o.txt().length())
                .reduce((a, b) -> a + b).getAsInt();
        System.out.println("fetched " + totalLength + " bytes from " + urls.size() + " urls in " + time + " ms");
    }
}

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

UrlTxt — это просто запись с двумя полями:

public record UrlTxt(String url,String txt) {}

Метод getUrlsFromUrl() извлекает абсолютные URL из содержимого https://www.bbc.com/news/world:

public static List<String> getUrlsFromUrl(String url) throws Exception {
    return Pattern.compile("href=\"(https:[^\"]+)\"")
            .matcher(get(url))
            .results()
            .map(r -> r.group(1))
            .collect(Collectors.toList());
}

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


Для получения содержимого URL используется обычный код:

public static String get(String url) throws Exception {
    var con = (HttpURLConnection) new URL(url).openConnection();
    con.setInstanceFollowRedirects(false);
    if (con.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
        return ""; // 404 throws FileNotFoundException 
    }
    try ( BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
        var response = new StringBuilder();
        String line;
        
        while ((line = in.readLine()) != null) {
            response.append(line);
        }
        
        return response.toString();
    }
}

get() используется в подклассе общего родителя Runner. Чтобы использовать get() асинхронным образом, я применяю метод-адаптер load(). Кстати, обратите внимание на раздражающее ограничение стандартных функциональных интерфейсов — они не выдают исключений и реализующий их код часто необходимо оборачивать в некрасивые блоки try catch.

public class URLRequests extends Runner {

    CompletableFuture<UrlTxt> load(String url) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return new UrlTxt(url, get(url));
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        });
    }

    @Override
    CompletableFuture<List<UrlTxt>> requestManyUrls(List<String> urls) throws InterruptedException, ExecutionException {
        CompletableFuture<UrlTxt>[] requests = urls
                .stream().map(url -> load(url)).toArray(i -> new CompletableFuture[i]);

        return CompletableFuture.allOf(requests)
                .thenApply(v -> {
                    return Stream.of(requests)
                            .map(future -> future.join())
                            .collect(Collectors.toList());
                });
    }

    public static void main(String[] args) Exception {
        new URLRequests().run();
    }
}

Функциональный код в requestManyUrls() адаптирован из самого современного рецепта по созданию параллельных запросов.

Результат работы кода:

fetched 39517285 bytes from 105 urls in 6211 ms

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

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


Похоже, в настоящее время HttpClient — это лучший класс Java для создания HTTP-запросов. Кажется, он даже поддерживает HTTP/2, потому что иногда выдаёт ошибку HTTP/2 GOAWAY.

public class HttpClientRequests extends Runner {
@Override
    public CompletableFuture<List<UrlTxt>> requestManyUrls(List<String> urls) throws InterruptedException, ExecutionException {
HttpClient client = HttpClient.newHttpClient();
CompletableFuture<HttpResponse<String>>[] requests = urls.stream()
                .map(url -> URI.create(url))
                .map(uri -> HttpRequest.newBuilder(uri))
                .map(reqBuilder -> reqBuilder.build())
                .map(request -> client.sendAsync(request, BodyHandlers.ofString()))
                .toArray(i -> new CompletableFuture[i]);
return CompletableFuture.allOf(requests)
                .thenApply(v -> {
                    return Stream.of(requests)
                            .map(future -> future.join())
                            .map(response -> new UrlTxt(response.uri().toString(), response.body()))
                            .collect(Collectors.toList());
                });
    }
public static void main(String[] args) throws Exception {
        new HttpClientRequests().run();
    }
}

Огромный код с современным HttpClient выглядит пугающе, но по сравнению с предыдущим результатом в 6211 мс его работа радует:

fetched 39983157 bytes from 105 urls in 4910 ms

Параллельные HTTP-запросы на Node.js


В браузере JavaScript не может скачивать содержимое с других хостов, если целевой хост этого не разрешил. Это мера безопасности. Сайт bbc.com не разрешает другим хостам получать его содержимое. Поэтому я использую только Node.js.

Посмотрите, насколько прост полный аналог предыдущего кода на JavaScript:

import fetch from 'node-fetch';
const re = /href=\"(https:[^\"]+)\"/g;
function extractLinks(txt) {
    return Array.from(txt.matchAll(re), ar => ar[1]);
}
function load(url) {
    return fetch(url,{redirect:"manual"})
        .then(res => res.text().then(txt => ({ url, txt })));
}
load("https://www.bbc.com/news/world")
    .then(({ txt }) => extractLinks(txt))
    .then(urls => {
        const start = Date.now();
        Promise.all(urls.map(url => load(url)))
            .then(contents => {
                const time= Date.now() - start ;
                const totalLength = contents.reduce((total, { url, txt }) =>  total + txt.length , 0);
                console.log("fetched " + totalLength + " bytes from " + urls.length + " urls in " + time + " ms");
            });
    });

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

Результат работы файла на JavaScript:

fetched 39492499 bytes from 105 urls in 1744 ms

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


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

В Java это работает совершенно иначе. Создаётся множество потоков, каждый из которых отправляет один HTTP-запрос. После создания некого оптимального количества потоков стандартный оптимальный внутренний пул потоков больше не может создавать потоки. Несколько созданных потоков ждут ответов. Код ничего не делает. После поступления ответов создаются новые потоки для отправки новых запросов. И этот процесс повторяется, пока не будут отправлены все запросы. По сути, мой пример кода на Java (4910–1744)/4910=64% от общего времени не делает ничего, кроме как ждёт HTTP-откликов. Ситуация такая же, как и с вводом-выводом в серверах приложений Java, но для Интернет-содержимого время ожидания больше.

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

Исходный код можно скачать с https://github.com/marianc000/concurrentHTTPRequests.

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


  1. dopusteam
    28.11.2021 10:23
    +21

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

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

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

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

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

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

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


    1. kalombo
      28.11.2021 11:03
      +6

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

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

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

      2. Цикл

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

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

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

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


      1. dopusteam
        28.11.2021 11:12
        -1

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

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

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

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

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

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

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


        1. kalombo
          28.11.2021 11:49
          +3

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

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

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

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

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


          1. dopusteam
            28.11.2021 11:57

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

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

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

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


        1. poxvuibr
          28.11.2021 14:41

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

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


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


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

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


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

          Можно.


    1. LEXA_JA
      28.11.2021 11:06
      +3

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


      1. cofolunat
        28.11.2021 14:57
        +9

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

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

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

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


      1. sshikov
        28.11.2021 17:36

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

        >Поток отправляет запрос и ждёт ответ. Количество потоков ограничено.
        Количество потоков ограничено в основном доступными ресурсами. И на вполне обычной машинке с 48 ядрами и 64 гигабайтами памяти я создавал порядка 15 тыс потоков без особых проблем. Хотя скорее всего это не эффективно, и как правило есть варианты получше — но этот лимит далеко не на уровне сотен потоков уж точно.


        1. vlanko
          28.11.2021 21:07

          Тут используется CompletableFuture.supplyAsync()
          Он работает на CommonPool и никакие новые потоки не создает. Судя по разнице в скорости, на машине было порядка 8-16 потоков


  1. kahi4
    28.11.2021 11:25
    +31

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

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


    1. faultedChip
      02.12.2021 10:05
      -1

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


      1. Vest
        02.12.2021 15:34
        +1

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


  1. apapacy
    28.11.2021 12:00
    +6

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



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


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


    1. 1fid
      28.11.2021 12:53

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


      1. apapacy
        28.11.2021 13:01
        -5

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


        1. FruTb
          28.11.2021 15:32
          +5

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


          1. sshikov
            28.11.2021 16:30

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


            1. FruTb
              29.11.2021 04:09

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


              1. sshikov
                29.11.2021 07:41
                +1

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

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


      1. faultedChip
        02.12.2021 20:38

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


  1. ultrinfaern
    28.11.2021 12:28
    +14

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


    1. pankraty
      28.11.2021 13:56
      +7

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


      1. sshikov
        28.11.2021 16:30
        +1

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


  1. Throwable
    28.11.2021 12:47
    +34

    Почему разница между 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 реквестами можно сразу выкинуть в корзину.


    1. WraithOW
      29.11.2021 12:35
      -5

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

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


    1. Vest
      29.11.2021 19:08
      +7

      Извините, я вас плюсанул, но попозже решил поподробнее поизучать код автора с помощью профайлинга кода на Яве и сетевых запросов как таковых. Попробую примазаться к вашей славе (хотя уже поздно).
      Сравнение, к сожалению, двух реализаций очень плохое. Просто потому что, если посмотреть на запросы, которые делает, 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();
          }

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


      1. Throwable
        29.11.2021 20:04

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

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


        1. Vest
          29.11.2021 20:15
          +1

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

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

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

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

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

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


  1. amarkevich
    28.11.2021 13:12
    +2

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

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

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


    1. mayorovp
      28.11.2021 13:52
      +2

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


      1. sshikov
        28.11.2021 15:35
        +1

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


  1. akurilov
    28.11.2021 14:01
    +4

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

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


    1. kornell
      28.11.2021 16:10
      +1

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

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


    1. petrov_engineer
      28.11.2021 16:59
      +1

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


      1. akurilov
        28.11.2021 17:05
        +2

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


        1. sshikov
          28.11.2021 17:12
          +2

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


          1. akurilov
            28.11.2021 18:19

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


            1. sshikov
              28.11.2021 18:39

              >Получение — тоже.
              Ну, чисто логически асинхронный код получения не такой простой, как синхронный код отправки запросов. А так-то для клиента и получение тоже не велика проблема, обычно.


        1. Saiv46
          28.11.2021 19:33
          +1

          `worker_threads` есть ещё с NodeJS v11, согласно документации они предназначены для CPU-bound задач. Только вот писать с его использованием мало кто хочет ввиду наличия (общих для всех ЯП) граблей многопоточности.


          1. sshikov
            28.11.2021 19:51

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


          1. akurilov
            28.11.2021 20:52
            -1

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


  1. ermadmi78
    28.11.2021 14:04
    +15

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

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

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


    1. ermadmi78
      28.11.2021 15:26
      +1

      PS.

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

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

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


      1. remal
        28.11.2021 19:22

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


        1. poxvuibr
          28.11.2021 19:34

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

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


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

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


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


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

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


        1. ermadmi78
          28.11.2021 22:57
          +2

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

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

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

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


          1. remal
            28.11.2021 23:09

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


            1. ermadmi78
              28.11.2021 23:13

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


    1. YuryB
      28.11.2021 22:31
      -2

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

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


      1. ermadmi78
        28.11.2021 22:37

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

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

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


        1. YuryB
          29.11.2021 11:16
          -2

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

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

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

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


          1. ermadmi78
            29.11.2021 11:23
            -1

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


            1. YuryB
              29.11.2021 11:34
              -1

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

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


              1. ermadmi78
                29.11.2021 11:37
                -1

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


                1. YuryB
                  29.11.2021 11:44

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


                  1. ermadmi78
                    29.11.2021 11:48

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


                    1. YuryB
                      29.11.2021 12:06

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


                      1. ermadmi78
                        29.11.2021 12:08

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

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

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


                      1. YuryB
                        29.11.2021 14:30
                        -1

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

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


                      1. poxvuibr
                        29.11.2021 13:25
                        +2

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

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


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


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


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


                  1. ermadmi78
                    29.11.2021 11:49

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

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

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


          1. mayorovp
            29.11.2021 11:49
            +3

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


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


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

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


            1. YuryB
              29.11.2021 11:54
              -1

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


      1. ermadmi78
        28.11.2021 22:45

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

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


  1. Revertis
    28.11.2021 14:50
    +8

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


  1. harios
    28.11.2021 14:51
    +2

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


    1. Alexufo
      29.11.2021 01:29
      -3

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


  1. mrbald
    28.11.2021 16:24
    +7

    КГ/АМ


  1. akurilov
    28.11.2021 16:54
    +1

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


    1. grossws
      28.11.2021 20:55

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


    1. FruTb
      29.11.2021 04:17

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

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

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


  1. Aprsta
    28.11.2021 18:19
    +1

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

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


    1. sshikov
      28.11.2021 19:26

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


  1. grossws
    28.11.2021 20:57
    +3

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


  1. alexdoublesmile
    29.11.2021 08:14
    +1

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


  1. GarretThief
    29.11.2021 09:54
    +1

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

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

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