Комментарии 20
А есть оценка реальной пользы от "виртуальных потоков". Декларируется, что выигрыш получается за счет уменьшения переключения контекста. Но засыпание и обычного, и "виртуального" потоков обычно связаны с какой-либо файловой операцией (в т.ч. сетевой операцией). А файловая операция обычно предполагает переключение контекста. Т.е. переключение контекста в итоге все равно происходит. И тогда какой смысл в виртуальных потоках..
Переключение между thread ОС - штука дорогая (связана с походом в ядро), и в реальной жизни, само переключение между несколькими десятками тысяч потоков займёт всё доступное время CPU.
Виртуальные потоки лёгкие и позволяют иметь сотни тысяч потоков без значимой нагрузки на систему.
Что касается операций io - то там другие методы борьбы с суровой реальностью (посмотрите epoll и родственные)
epoll (который сейчас обычно используется) как раз предполагает переключение контекста. Т.е. от переключения контекста "виртуальные потоки" не избавляют. Вот и вопрос, какой от них реальный толк ?
Кроме того, если их реально много, не стоит и про размер стека забывать, 1000 потоков уже 1 Гб.
Если код исполняется в виртуальном потоке, Java под капотом вызовет вместо блокирующего API операционной системы, более оптимальный неблокирующий аналог, такой как epoll. Виртуальные потоки - это по сути задачи, которые исполняет отдельный ForkJoinPool. Когда исполнение дошло до блокирующей операции, Java заменяет её на неблокирующий аналог, на результат подписывается отдельный специальный поток-unparker и как только от ОС поступает сигнал, что данные получены, этот unparker отправляет задачу обратно в ForkJoinPool, чтобы она продолжила исполняться.
Важно, что в ForkJoinPool не должно происходить никаких блокировок, иначе получаем pinning-проблему, вероятность который всё меньше и меньше с каждой новой версией джавы )
Чем больше блокирующих операций, или чем они дольше, тем больше бенефит от такого подхода. Но кажется, тут каждый отдельный кейс уникален и без нагрузочного тестирования не обойтись. Благо, это не сложно написать код так, чтобы виртуальные потоки можно было легко выключить, если нагрузочное тестирование покажет, что выигрыша от них нет. Благо контракты не меняются: VirtualThread extends Thread и есть реализация ExecutorService, которая использует виртуальные потоки. Также есть флаг в spring boot и многих других фреймворках, чтобы легко включить или выключить виртуальные потоки:
spring.threads.virtual.enabled: true
Пожалуйста, не путайте Reactor (то, что вы имели ввиду) и React (JS SPA-фреймворк).
Несмотря на то, что виртуальные потоки в Java 21 релизнулись, всё-таки их очень сложно использовать вне контекста, как вы сказали, микросервиса http-аггрегатора. Далеко не каждый микросервис такой, ну только если частично внедрять, но там уже риски случайно использовать не тот пул и попасть на пиннинг из-за synchronized.
Я пытался эту проблему зарешать в общем виде с помощью агента, но потерпел фиаско :) Даже внутри ConcurrentHashMap есть synchronized... https://github.com/SimSonic/reentrantlock-java21agent
Стоило бы упомянуть, что наконец-то в Java 24 проблему пиннинга на нём победили, и если вам интересны виртуальные потоки, то для вас скорее 24 — уже необходимый минимум.
Да, имелся в виду Project Reactor.
Согласен, что внедрять надо аккуратно. В случае http-аггрегатора польза будет максимальная, но возможны другие кейсы. Например, если сервис получает обновления в реальном времени с биржи по веб-сокетам и грузит в какую-то внутреннюю систему: здесь пропускная способность очень важна и можно рассмотреть использование виртуальных потоков.
Что касается synchronized - действительно часто встречается внутри ThreadSafe структур данных. Но важно помнить, что с проблемой столкнёмся только в том случае, если внутри synchonized блока есть блокирующая операция, или вызов wait метода. В Java 21 есть стандартный способ проверить, что нет проблемы с pinning: есть флаг -Djdk.tracePinnedThreads=full. И, чтобы проверить, что мониторинг пиннинга реально работает, можно специально, при старте сервиса запустить код, который вызовет пиннинг на секунду, при старте сервиса:
BlockingOperation.java:
import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class BlockingOperation implements Runnable {
private final Duration duration;
@Override
public void run() {
try {
sleep(duration.toMillis());
} catch (InterruptedException e) {
currentThread().interrupt();
}
}
}
SyncronizedBlockingOperation.java:
import static java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor;
import java.time.Duration;
public class SynchronizedBlockingOperation extends BlockingOperation {
public SynchronizedBlockingOperation(Duration duration) {
super(duration);
}
public static void createAndRun(Duration duration) {
try (final var executor = newVirtualThreadPerTaskExecutor()) {
executor.execute(new SynchronizedBlockingOperation(duration));
}
}
@Override
public synchronized void run() {
super.run();
}
}
В main методе:
package ru.rshb.fixadapter;
import java.time.Duration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FixAdapterApplication {
public static void main(String[] args) {
SpringApplication.run(FixAdapterApplication.class, args);
SynchronizedBlockingOperation.createAndRun(Duration.ofSeconds(1));
}
}
В Java 24 проблему действительно решили. Там пиннинг практически невозможно вызвать. Но теоретически всё ещё возможно, при вызове блокирующей операции из нативного кода.
Мы у себя в компании часть сервисов обновим до Java 25 LTS сразу, спустя несколько месяцев после того как она выйдет осенью этого года.
Не говоря про ужасный русский, замечу по сути: сначала говорится, что виртуальный поток с блокирубщей операцией просто снимается с реального потока:
трюк, который называется park and mount: в этот момент у нас виртуальная задача с блокирующей операцией снимается с воркера. И наш воркер спокойно продолжает заниматься другими задачами.
Потом говорится ровно обратное - что реальный поток тоже блокируется:
наши виртуальные потоки просто перестают работать в таком кейсе: если вызвать блокирующую операцию, то у нас воркер заблокируется.
Причём, там нет ещё ни слова про synchronized.
Как-то надо тщательнее.
В целом, очень сумбурные текст.
Мда. Во-первых, надо поработать над языком, написано ужасно. Во-вторых, надо поработать над оформлением, набор красно-чёрных картинок занимает 70% вертикального пространства статьи, нафиг не надо. В третьих, надо поработать над структурой.
Перевод плохой, но вкратце что я прочитал:
в Java добавили стекфул корутины и заменили ими старые треды. Назвали их опять по новому, потому что надо придумать базворд менеджеру джавы
добавили плохо, с очень плохим сломом обратной совместимости, так что неявные мьютексы создаваемые в sync блоках могут анлочиться на другом потоке и это undefined behavior
тредпул с кражей (если точнее - написали тредпул как вышло, долго не думая), ошибка новичка, перфоманс скорее всего хуже чем было до
Это статья написана по итогам выступления Ивана на конференции. Перевода никакого не было.
Сказ про то, как в Java наконец добавили TPL, но до конца не сдюжили. На самом деле в 24-й допилили кое-что.
Ваш п.3 - общая проблема, любая смесь тасков и классических потоков внутри одного флоу может привести к проблемам блокировки. К счастью, есть механизмы, которые могут заставить отдельные таски выполняться в рамках только одного рабочего потока, но это может привести уже к неэффективности.
Полотно из изображений на 20 экранов вызвало истерический смех
А ничего, что уже 25 джава скоро и про эти потоки уже писали на Хабре?
С терминологией всё как то совсем плохо:
Нам обещают, что в следующей Java синхрониза блоков можно будет не бояться.
Может "synchronized блоки" имелись ввиду? Термин synchronized в русскоязычной технической литературе не принято переводить на русский. И, даже если и переводить, то как "синхронизированный блок" а не как "синхрониза блока".
Можно не бояться писать простой блокирующий код, который не возвращает CompatibleFuture.
Во первых, что такое "CompatibleFuture" в Java? Где вы такое увидели? Может вы хотели сказать "CompletableFuture"?
Во, вторых, где вы видели "блокирующий код", возвращающий "CompletableFuture"? Это же масло маслянное получается. Весь смысл "CompletableFuture" состоит в том, что её возвращает "неблокирующий код", запуская асинхронную операцию в другом потоке, которая оповещает о своём завершении и результате выполнения операции с помощью "CompletableFuture". А блокироваться на Future или нет - это как вызывающий поток решит.
И почему я должен бояться "блокирующего кода", который зачем то вернёт мне "CompletableFuture", и не бояться "простого блокирующего кода" который возможно начнёт выполнять тяжелые вычисления, или, хуже того, I/O операции?
PS
Стойкое ощущение, что у автора просто каша в голове. Читайте лучше спецификацию, а не такие статьи. Там простым понятным языком написано:
The operating system schedules when a platform thread is run. However, the Java runtime schedules when a virtual thread is run. When the Java runtime schedules a virtual thread, it assigns or mounts the virtual thread on a platform thread, then the operating system schedules that platform thread as usual. This platform thread is called a carrier. After running some code, the virtual thread can unmount from its carrier. This usually happens when the virtual thread performs a blocking I/O operation. After a virtual thread unmounts from its carrier, the carrier is free, which means that the Java runtime scheduler can mount a different virtual thread on it.
A virtual thread cannot be unmounted during blocking operations when it is pinned to its carrier. A virtual thread is pinned in the following situations:
The virtual thread runs code inside a
synchronized
block or methodThe virtual thread runs a
native
method or a foreign function (see Foreign Function and Memory API)Pinning does not make an application incorrect, but it might hinder its scalability. Try avoiding frequent and long-lived pinning by revising
synchronized
blocks or methods that run frequently and guarding potentially long I/O operations with java.util.concurrent.locks.ReentrantLock.
Новая фича в Java 21: Виртуальные потоки: новые возможности для I/O bound микросервисов