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

Я изучал, как работает Project Loom и какие преимущества он может принести моей компании.

Итак, я понимаю мотивацию для стандартного бэкэнда на основе сервлета, всегда есть пул потоков, который выполняет бизнес-логику, как только поток блокируется из-за ввода-вывода, он не может ничего делать, кроме как ждать. Итак, скажем, у меня есть серверное приложение с одной конечной точкой, бизнес-логика этой конечной точки заключается в чтении некоторых данных с использованием JDBC, который внутренне использует InputStream, который снова будет использовать блокирующий системный вызов (read() с точки зрения Linux). Итак, если у меня есть 200 сотен пользователей, достигающих этой конечной точки, мне нужно создать 200 потоков, каждый из которых ожидает ввода-вывода.

Теперь предположим, что я переключил пул потоков на использование виртуальных потоков. Согласно Бену Эвансу в статье Going inside Java Project Loom and virtual threads:

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

Насколько я понимаю, если у меня количество потоков ОС равно количеству ядер ЦП и неограниченному количеству виртуальных потоков, все потоки ОС все равно будут ждать ввода-вывода, а служба исполнителя не сможет назначить новую работу для виртуальных потоков. потому что нет доступных потоков для его выполнения. Чем он отличается от обычных потоков, по крайней мере, для потоков ОС я могу масштабировать его до тысячи, чтобы увеличить пропускную способность. Или я просто неправильно понял вариант использования Loom? заранее спасибо

Добавить

Я только что прочитал этот список рассылки :

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

Я не уверен, что понимаю это, у ОС нет возможности освободить поток, если он выполняет блокирующий вызов, такой как чтение, для этих целей ядро ​​​​имеет неблокирующие системные вызовы, такие как epoll, который не блокирует поток и немедленно возвращает список файловых дескрипторов, для которых доступны некоторые данные. Означает ли приведенная выше цитата, что под капотом JVM заменит блокирующий readбез блокировки epollесли поток, вызвавший его, является виртуальным?

3 ответа

В вашем первом отрывке отсутствует важный момент:

Вместо этого виртуальные потоки автоматически отказываются (или уступают) своему потоку-носителю, когда выполняется блокирующий вызов (например, ввод-вывод). Этим занимается библиотека и среда выполнения [...]

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

Только если ни один виртуальный поток не готов к выполнению, собственный поток будет припаркован.

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

Я наконец нашел ответ. Как я уже сказал, по умолчанию InputStream.readметод делает read()syscall, который, согласно справочным страницам Linux, блокирует подчиненный поток ОС. Так как же возможно, что Loom не заблокирует его? Я нашел статью , в которой показана трассировка стека. Итак, если этот блок кода будет выполняться виртуальным потоком

      URLData getURL(URL url) throws IOException {
  try (InputStream in = url.openStream()) {//blocking call
    return new URLData(url, in.readAllBytes());
  }
}

Время выполнения JVM преобразует его в следующую трассировку стека

      java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:60)//this line parks the virtual thread
java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:184)
java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:212)
java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:356)//JVM runtime will replace an actual read() into read from java nio package 
java.base/java.io.InputStream.readAllBytes(InputStream.java:346)

Как JVM знает, когда разблокировать виртуальный поток? Вот трассировка стека, которая будет запущена один раз readAllBytesзакончен

      "Read-Poller" #16
  java.base@17-internal/sun.nio.ch.KQueue.poll(Native Method)
  java.base@17-internal/sun.nio.ch.KQueuePoller.poll(KQueuePoller.java:65)
  java.base@17-internal/sun.nio.ch.Poller.poll(Poller.java:195)

Автор статьи использует MacOs, Mac использует kqueueкак неблокирующий системный вызов. Если я запущу его в Linux, я увижу системный вызов.

Так что по сути Loom не вводит ничего нового, под капотом это обычный epollsyscall с обратными вызовами, которые могут быть реализованы с использованием фреймворка, такого как Vert.x, который использует Netty под капотом, но в Loom логика обратного вызова инкапсулирована во время выполнения JVM, что мне показалось нелогичным, когда я вызываю InputStream.read(), я делаю ожидать соответствующий системный вызов read(), но JVM заменит его неблокирующими системными вызовами.

Ответ Томаса Клагера правильный. Добавлю несколько мыслей.

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

Нет, неправильно, вы неправильно поняли.

То, что вы описываете, - это то, что происходит в современной технологии потоковой передачи в Java. При однозначном сопоставлении потока Java с потоком хост-ОС любой вызов, сделанный в Java, который блокируется (относительно долгое время ожидает ответа), оставляет этот хост-поток бездействующим. Это не было бы проблемой, если бы хост имел миллионы потоков, чтобы другие потоки могли быть запланированы для работы на ядре ЦП. Но потоки хост-ОС довольно дороги, поэтому у нас их не миллион, их очень мало.

Используя технологию Project Loom, JVM обнаруживает блокирующий вызов, например ожидание ввода-вывода. После обнаружения JVM откладывает («паркует») виртуальный поток, ожидая ответа ввода-вывода. JVM назначает другой виртуальный поток этому несущему потоку ОС хоста, так что «настоящий» поток может продолжать выполнять работу, а не ждать, сложа руки. Поскольку виртуальные потоки, живущие в JVM, настолько дешевы (высокоэффективны как с памятью, так и с ЦП), у нас могут быть тысячи, даже миллионы, чтобы JVM могла жонглировать.

В вашем примере 200 потоков, каждый из которых ожидает ответа IO от JDBC, вызывает базу данных, если бы это были виртуальные потоки, которые все были бы припаркованы в JVM. Несколько потоков операционной системы хоста, используемых в качестве потоков-носителей вашим ExecutorServiceбудет работать над другими виртуальными хлебами, которые в настоящее время не заблокированы. Эта парковка и перепланирование заблокированных, а затем разблокированных виртуальных потоков автоматически обрабатывается технологией Project Loom в JVM без вмешательства разработчиков Java-приложений.

скажем, я переключил пул потоков на использование виртуальных потоков

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

      ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor() ;
…
executorService.submit( someTask ) ;  // Every task submitted gets assigned to a fresh new virtual thread.

Чтобы узнать больше, я настоятельно рекомендую просмотреть видеоролики презентаций и интервью Рона Пресслера или Алана Бейтмана, членов команды Project Loom. Найдите самую последнюю версию, так как Loom развивается.

И прочитайте новый Java JEP, проект JEP: Virtual Threads (Preview).

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