Различается ли `Thread.sleep` с виртуальными потоками (волокнами) в Project Loom для Java
я используюпри экспериментировании или демонстрации кода Java для параллелизма . Во сне я имитирую некую работу по обработке, которая займет некоторое время.
Мне интересно сделать это в рамках .
- В рамках технологии Project LoomProject Loom с виртуальными нитями (волокнами) можно ли использовать
Thread.sleep
таким же образом? - Есть ли что-то отличное или примечательное в спящем виртуальном потоке по сравнению со спящим потоком платформы/ядра?
В целях самообразования я посмотрел несколько видеороликов конца 2020 года, в которых Рон Пресслер из Oracle представляет технологию Project Loom (здесь , здесь ). Просвещая, я не припомню, чтобы он обращался к вопросу о том, чтобы спать нить.
3 ответа
- Можем ли мы в рамках технологии Project Loom с виртуальными потоками (волокнами) использовать Thread.sleep таким же образом?
Оказывается так. Я имею в виду страницу в вики OpenJDK, посвященную операциям блокировки в Loom . В нем перечислены
Thread.sleep()
среди операций, дружественных к виртуальным потокам, что означает, что
Когда они не закреплены, они освобождают базовый поток-носитель для выполнения другой работы, когда операция блокируется.
Вы продолжаете спрашивать,
- Есть ли что-то отличное или примечательное в спящем виртуальном потоке по сравнению со спящим потоком платформы/ядра?
Документация скудна, и неясно, являются ли какие-либо различия, которые могут действительно существовать, преднамеренными. Тем не менее, я склонен думать, что цель спящего виртуального потока состоит в том, чтобы иметь семантику, максимально близкую к семантике спящего обычного потока. Я подозреваю, что у достаточно умной программы будут способы отличить, но если бы были какие-то различия, которые поднялись до уровня «заслуживающих внимания», то я ожидаю, что они будут считаться ошибками. Я основываю это частично на предположениях, но я также отсылаю вас к документу State of Loom на java.net, в котором среди его «ключевых выводов» перечислены следующие:
- Виртуальный поток — это поток — в коде, во время выполнения, в отладчике и в профилировщике.
а также
- Никаких изменений языка не требуется .
(Выделение добавлено.)
Ответ Джона Боллинджера и Ответ Стивена С. являются правильными и информативными. Я подумал, что добавлю пример кода, чтобы показать:
- Как виртуальные потоки, так и потоки платформы/ядра уважают .
- Поразительное увеличение производительности возможно благодаря технологии Project Loom.
Сравнительный код
Давайте просто напишем цикл. В каждом цикле мы создаем экземпляр
System.nanoTime
. Наконец, мы выводим это число на консоль.
Но фишка в том, что перед вычислением мы засыпаем поток, выполняющий эту задачу. Поскольку каждый спит в течение начальных двенадцати секунд, мы не должны видеть ничего на консоли, пока не пройдет как минимум 12 секунд мертвого времени.
Затем представленные задачи выполняют свою работу.
Мы запускаем это двумя способами, включая/отключая пару закомментированных строк.
-
Обычный пул обычных потоков, использующий 5 из 6 реальных ядер (без гиперпоточности) на этом Mac mini (2018 г.) с процессором Intel Core i5 3 ГГц и 32 ГБ ОЗУ. -
Служба исполнителя, поддерживаемая новыми виртуальными потоками (волокнами), предоставляемыми Project Loom в этой специальной сборке раннего доступа к Java 16.
package work.basil.example;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TooFast
{
public static void main ( String[] args )
{
TooFast app = new TooFast();
app.demo();
}
private void demo ( )
{
System.out.println( "INFO - starting `demo`. " + Instant.now() );
long start = System.nanoTime();
try (
// 5 of 6 real cores, no hyper-threading.
ExecutorService executorService = Executors.newFixedThreadPool( 5 ) ;
//ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
)
{
Duration sleep = Duration.ofSeconds( 12 );
int limit = 100;
for ( int i = 0 ; i < limit ; i++ )
{
executorService.submit(
new Runnable()
{
@Override
public void run ( )
{
try {Thread.sleep( sleep );} catch ( InterruptedException e ) {e.printStackTrace();}
long x = ( System.nanoTime() - 42 );
System.out.println( "x = " + x );
}
}
);
}
}
// With Project Loom, the flow-of-control blocks here until all submitted tasks have finished.
Duration demoElapsed = Duration.ofNanos( System.nanoTime() - start );
System.out.println( "INFO - demo took " + demoElapsed + " ending at " + Instant.now() );
}
}
Полученные результаты
Результаты поразительны.
Во-первых, в обоих случаях мы видим задержку чуть более 12 секунд перед любой активностью консоли. Итак, мы знаем, что на самом деле выполняются как потоки платформы/ядра, так и виртуальные потоки.
Во-вторых, виртуальные потоки выполняют все задачи за считанные секунды по сравнению с минутами, часами или днями для обычных потоков.
Со 100 задачами:
- Обычная резьба занимает 4 минуты (PT4M0.079402569S).
- Виртуальные потоки занимают чуть более 12 секунд (PT12.087101159S).
С 1000 задач:
- Обычная резьба занимает 40 минут (PT40M0.667724055S).
(Это имеет смысл: 1000 * 12/5/60 = 40) - Виртуальные потоки занимают 12 секунд (PT12.177761325S).
С 1 000 000 задач:
- Обычные потоки занимают… ну, дни.
(На самом деле я не стал ждать. Ранее я испытал 29-часовое выполнение полумиллиона циклов в более ранней версии этого кода.) - Виртуальные потоки занимают 28 секунд (PT28.043056938S).
(Если вычесть 12 секунд бездействия, проведенного в спящем режиме, миллион потоков, выполняющих всю свою работу за оставшиеся 16 секунд, дает около 62500 многопоточных задач в секунду, выполняемых с немедленным выполнением.)
Вывод
С обычными потоками мы можем видеть повторяющийся пакет из нескольких строк, внезапно появляющихся на консоли. Таким образом, мы можем видеть, как потоки платформы/ядра на самом деле находятся на ядре, заблокированные, пока они ждут своего 12-секундного
В качестве отступления: я бы предположил, что хост - ОС замечает, что наши потоки Java на самом деле ничего не делают, а затем использует свой планировщик ЦП для приостановки наших потоков Java, пока они заблокированы, чтобы позволить другим процессам, таким как другие приложения, использовать ядра ЦП. Но если так, то это прозрачно для нашей JVM. С точки зрения JVM, спящие потоки Java занимают процессор в течение всего сна.
С виртуальными потоками мы наблюдаем совершенно другое поведение. Project Loom разработан таким образом, что когда виртуальный поток блокируется, JVM перемещает этот виртуальный поток из потока платформы/ядра и помещает на его место другой виртуальный поток. Такое переключение потоков внутри JVM намного дешевле, чем переключение потоков платформы/ядра. Поток платформы/ядра, несущий эти различные виртуальные потоки, может оставаться занятым, а не ждать прохождения каждого блока.
Для получения дополнительной информации см. любой из недавних (конец 2020 г.) докладов Рона Пресслера из Project Loom в Oracle и его статью 2020–2005 гг . State of Loom . Такое поведение быстрого обмена заблокированными виртуальными потоками настолько эффективно, что ЦП может быть постоянно занят. Мы можем подтвердить этот эффект в приложении Activity Monitor . Вот скриншот монитора активности , выполняющего миллион задач с виртуальными потоками. Обратите внимание, что ядра процессора загружены практически на 100% после того, как все миллионы потоков перестают спать в течение 12 секунд.
Таким образом, вся работа фактически выполняется немедленно, так как все миллионы потоков одновременно спали на 12 секунд, в то время как потоки платформы/ядра спали последовательно группами по пять. На этом снимке экрана выше мы видим, как работа миллиона задач выполняется одновременно за считанные секунды, в то время как потоки платформы/ядра выполняют тот же объем работы, но распределяют ее в течение нескольких дней.
Обратите внимание, что такое резкое увеличение производительности происходит только тогда, когда ваши задачи часто блокируются. При использовании задач, связанных с ЦП, таких как кодирование видео, вам следует использовать потоки платформы/ядра, а не виртуальные потоки. Большинство бизнес-приложений сталкиваются с большим количеством блокировок, таких как ожидание обращений к файловой системе, базе данных, другим внешним службам или сети для доступа к удаленным службам. Виртуальные потоки прекрасно справляются с такой часто блокируемой рабочей нагрузкой.
Глядя на исходный код , когда вы звоните
sleep(...)
в виртуальном потоке он обрабатывается планировщиком виртуальных потоков JVM; т.е. без непосредственного выполнения системного вызова и без блокировки собственного потока.
Так:
В рамках технологии Project Loom с виртуальными нитями (волокнами) можно ли использовать
Thread.sleep
таким же образом?
Да.
Есть ли что-то другое или примечательное в спящем виртуальном потоке по сравнению со спящим потоком платформы/ядра?
Спящий виртуальный поток обрабатывается так, как вы ожидаете от виртуального потока. Производительность будет отличаться от потока ядра, но поведение предназначено для прозрачного кода приложения... который не делает необоснованных предположений о поведении планировщика потоков.
Во всяком случае, javadocs для
Thread.sleep(...)
в Loom в настоящее время не упоминаются какие-либо различия между ядром и виртуальными потоками.