Проект ткацкий станок, что происходит, когда виртуальный поток делает блокирующий системный вызов?
Я изучал, как работает 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 не вводит ничего нового, под капотом это обычный
epoll
syscall с обратными вызовами, которые могут быть реализованы с использованием фреймворка, такого как 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).