Что именно делает виртуальные потоки Java лучше

Я в восторге от Project Loom, но есть одна вещь, которую я не могу полностью понять.

Большинство серверов Java используют пулы потоков с определенным лимитом потоков (200, 300 ..), однако вы не ограничены ОС, чтобы создавать гораздо больше, я читал, что со специальными конфигурациями для Linux вы можете достичь огромных чисел.

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

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

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

      public static void main(String[] args) {
    for (int i = 0; i < 1_000_000; i++) {
        Thread.startVirtualThread(() -> {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

приведенный выше код прерывается примерно на 25 КБ с исключением OOM, когда я использую потоки платформы.

Мой вопрос в том, что именно делает эти потоки такими легкими, что мешает нам создать 1 миллион потоков платформы и работать с ними, только ли переключение контекста делает обычные потоки такими «тяжелыми».

Один очень похожий вопрос

Вещи, которые я нашел до сих пор:

  • Переключение контекста стоит дорого. Вообще говоря, даже в идеальном случае, когда ОС знает, как будут вести себя потоки, ей все равно придется предоставить каждому потоку равные шансы на выполнение, учитывая, что они имеют одинаковый приоритет. Если мы создадим 10 000 потоков ОС, ей придется постоянно переключаться между ними, и одна только эта задача в некоторых случаях может занимать до 80% процессорного времени, поэтому мы должны быть очень осторожны с цифрами. С виртуальными потоками переключение контекста выполняется JVM, что делает его практически бесплатным.
  • Дешевый старт/стоп. Когда мы прерываем поток, мы, по сути, сообщаем задаче: «Убить поток ОС, в котором вы работаете». Однако, если, например, этот поток находится в пуле потоков, к тому времени, когда мы запрашиваем, поток может быть освобожден текущей задачей, а затем передан другой, и другая задача может получить сигнал прерывания. Это делает процесс прерывания довольно сложным. Виртуальные потоки — это просто объекты, которые живут в куче, мы можем просто позволить сборщику мусора собирать их в фоновом режиме.
  • Жесткие верхние пределы (максимум десятки тысяч) потоков из-за того, как ОС их обрабатывает. ОС не может быть точно настроена на конкретные приложения и язык программирования, поэтому она должна подготовиться к наихудшему сценарию с точки зрения памяти. Он должен выделить больше памяти, которая фактически будет использоваться для удовлетворения всех потребностей. Делая все это, он должен убедиться, что жизненно важные процессы ОС все еще работают. С VT вы ограничены только дешевой памятью.
  • Поток, выполняющий транзакцию, ведет себя совершенно иначе, чем поток, выполняющий обработку видео. Опять же, ОС должна подготовиться к наихудшему сценарию и приспособиться к обоим случаям наилучшим образом, что означает, что в большинстве случаев мы получаем неоптимальную производительность. Поскольку VT создаются и управляются самой Java, это обеспечивает полный контроль над ними и оптимизацию конкретных задач, которые не привязаны к ОС.
  • Изменяемый размер стека. ОС предоставляет потокам большой стек, подходящий для всех вариантов использования, виртуальные потоки имеют изменяемый размер стека, который находится в пространстве кучи, он динамически изменяется в соответствии с проблемой, что делает его меньше.
  • Меньший размер метаданных. Потоки платформы используют 1 МБ, как указано выше, тогда как виртуальным потокам требуется 200-300 байт для хранения своих метаданных.

4 ответа

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

Что именно делает Java Virtual Threads лучше?

Преимущества виртуальных потоков

  • демонстрирует точно такое же поведение, как и потоки платформы.
  • одноразовые и могут быть масштабированы до миллионов.
  • намного легче, чем платформенные потоки.
  • быстрое время создания, такое же быстрое, как создание строкового объекта.
  • JVM выполняет ограниченное продолжение операций ввода-вывода, без ввода-вывода для виртуальных потоков.
  • но может иметь последовательный код, как и предыдущий, но более эффективный.
  • JVM создает иллюзию виртуальных потоков, а вся история идет о потоках платформы.
  • Просто с использованием ядра ЦП с виртуальным потоком становится намного более параллельным, комбинация виртуальных потоков и многоядерного ЦП с ComputableFutures для распараллеливания кода очень эффективна.

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

  • Не используйте монитор, т.е. синхронизированный блок, однако это будет исправлено в новой версии JDK, альтернативой этому является использование «ReentrantLock» с оператором try-final.

  • Блокировка с помощью собственных фреймов в стеке, JNI. это очень редко

  • Контроль памяти на стек (уменьшение локалей потока и отсутствие глубокой рекурсии)

  • Еще не обновленные инструменты мониторинга, такие как отладчики, JConsole, VisualVM и т. д.

  • Платформенные потоки против виртуальных потоков. Потоки платформы берут в заложники потоки ОС в задачах и операциях на основе ввода-вывода, ограниченных числом применимых потоков с пулом потоков и потоками ОС, по умолчанию они не являются потоками демона.

  • Виртуальные потоки реализованы с помощью JVM, в операциях с привязкой к ЦП, связанных с потоками платформы, и их перенастройке в пул потоков, после завершения операции с привязкой ввода-вывода из пула потоков будет вызываться новый поток, поэтому в этом случае нет заложников.

Архитектура четвертого уровня для лучшего понимания.

Процессор

  • Многоядерные многоядерные процессоры с выполнением операций в процессоре.

Операционные системы

  • Потоки ОС планировщик ОС выделяет время ЦП задействованным потокам ОС.

JVM

  • потоки платформы полностью оборачивают потоки ОС с обеими задачами
  • виртуальные потоки связаны с потоками платформы в каждой операции, связанной с ЦП, каждый виртуальный поток может быть связан с несколькими потоками платформы в разное время.

Виртуальные потоки со службой Executer

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

             try(ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()) {
         service.submit(ExecutorServiceVirtualThread::taskOne);
         service.submit(ExecutorServiceVirtualThread::taskTwo);
     }
    
  • Служба исполнителя реализует интерфейс Auto Closable в JDK 19, таким образом, при использовании с «попыткой с ресурсом», как только он достигнет конца «попытки», заблокируйте вызываемый «закрыть» API, в качестве альтернативы основной поток будет ждать, пока все отправленные задачи с их выделенные виртуальные потоки завершают свой жизненный цикл, и соответствующий пул потоков отключается.

             ThreadFactory factory = Thread.ofVirtual().name("user thread-", 0).factory();
     try(ExecutorService service = Executors.newThreadPerTaskExecutor(factory)) {
         service.submit(ExecutorServiceThreadFactory::taskOne);
         service.submit(ExecutorServiceThreadFactory::taskTwo);
     }
    
  • Служба исполнителя также может быть создана с виртуальной фабрикой потоков, просто поставив фабрику потоков с аргументом конструктора.

  • Может использовать функции службы Executer, такие как Future и Completable Future.

Узнайте больше о JEP-425

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

позвольте мне сначала представить закон Литтла:

      concurrency = arrival_rate * latency

И мы можем переписать это так:

      arrival_rate = concurrency/latency

В стабильной системе скорость поступления равна пропускной способности.

      throughput = concurrency/latency

Для увеличения пропускной способности у вас есть 2 варианта:

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

С обычными потоками трудно достичь высокого уровня параллелизма с блокирующими вызовами из-за накладных расходов на переключение контекста. Запросы могут быть отправлены асинхронно в некоторых случаях (например, привязка NIO + Epoll или Netty io_uring), но тогда вам нужно иметь дело с обратными вызовами и адом обратных вызовов.

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

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

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

Вот почему Golang пробился в индустрию (помимо поддержки Google). Горутины — это концепция, очень похожая на виртуальные потоки Java, и они решают ту же проблему.

Существуют и другие способы добиться того, что делает виртуальный поток (например, NIO и связанный с ним шаблон Reactor). Это, однако, влечет за собой использование циклов сообщений и обратных вызовов, которые искажают ваш разум (вот почему так много людей ненавидят JavaScript). Поверх них есть слои абстракций, которые немного упрощают работу, но они также имеют свою цену.

Конечно, хотелось бы, чтобы люди указали, о какой ОС они говорят. Я сильно подозреваю, что потоки Java имеют преимущество в производительности в Windows, но не в Linux.

Другие вопросы по тегам