Как стать автором
Обновить

Project Loom: Современная маcштабируемая многопоточность для платформы Java

Время на прочтение27 мин
Количество просмотров25K
Всего голосов 33: ↑33 и ↓0+33
Комментарии32

Комментарии 32

НЛО прилетело и опубликовало эту надпись здесь
Спасибо, что заметили ошибку. Обновила эту часть

У вас ещё в паре мест такое

Еще раз всё перепроверили и вроде бы везде исправили, но если вы или кто-то еще заметите, что не везде, то пишите

Сразу перед разделом "Ограничения".


Вообще, было бы неплохо перед публикацией дать вычитать кому-то, кто разбирается в том, что написано. Я, например, долго ломал голову, почему вывод результатов curl заканчивается "$", хотя Java ничего такого не генерирует. Видимо, потому что из консоли так скопировали, без понимания того, что это.
А больше всего я думал над тем, что такое "закрепление потока ОС".

Спасибо за указание, поправили.

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

Насчет «закрепление потока ОС» — а как это лучше перевести, если нет устоявшейся нормы? В докладе Алан говорит «what we call «pin» underlying system thread», то есть вроде бы сам подчеркивает, что это не какое-то устоявшееся всем знакомое понятие с однозначным переводом.

Это действительно неустоявшийся термин, поэтому на мой взгляд, можно было его перевести его более развернуто. Судя по тому, что я нашел, правильнее переводить не просто "закрепление потока ОС" (что по-русски имеет мало смысла), а "закрепление потока ОС за виртуальным потоком" (примерно как закрепление рабочего места за сотрудником).

Очень похоже, что это код из идеи вместе с её подсказками выводимых типов. На стриме Алан демки показывал именно в ней.

Выглядит многообещающе для самого java-приложения. Но я вот не понимаю одного: выигрыш здесь от I/O-операций и им подобных, это значит, что приложение будет взаимодействовать с другими компонентами, например БД или другой микросервис. Пул потоков, от которого предлагают отказаться, предоставляет удобный способ настраиваемо ограничивать количество одновременных операций, выполняемых на взаимодействующей стороне. На его фоне предлагаемые ручные блокировки как ограничитель выглядят вообще велосипедом. (Или они действительно предлагают позволить приложению постоянно выполнять миллионы запросов в БД?)

Просто нативные нити не позволят обрабатывать большое кол-во одновременных задач (нп соединений). Процесс загнется на сотнях или тысячах. Легковесные нити позволяют эффективно обрабатывать миллион одновременных задач.

Процесс загнется на сотнях или тысячах. Легковесные нити позволяют эффективно обрабатывать миллион одновременных задач.

ИМХО, это нужная фича, но по-моему проблему перувеличивают.
Десятки тысяч на обычном десктопе(не сервер) легко запускаются.
А в луме миллионы тоже скорее теоретическое количество-на практике не выходит сделать миллион.
Ну и как часто у вас возникают ситуации что вам нужны миллионы потоков?


В подтверждение приведу выдержку из статьи которыую скинул ниже, про сравнение количества потоков в kernek threads и в virtual threads


The kernel threads used 3.7GB/32K = 114KB/thread of heap memory, whilst the Loom virtual threads used 8.4GB/55K = 152KB/thread of heap. The additional heap memory usage for virtual threads appears to be due to the stacks being stored on the heap rather than in kernel memory.

Of course, both of these tests could be tuned with heap sizes, stack sizes, etc. but the key point is that both approaches are both more or less limited by memory and are both the same order of magnitude. Both tests achieve 10,000s of threads and both could probably peak above 100,000 with careful tuning. Neither is hitting 100,000s let alone a 1,000,000.

Loom does allow you to have many threads, even 1,000,000 threads, but not if those threads have deep stacks. It appears to increase total memory usage for stack entries and also comes at a cost of long garbage collections. These are significant limitations on virtual threads, so they are not a Silver Bullet solution that can be used as a drop-in replacement for kernel threads.

См. историю nginx и проблему c10k. На десктопе запустятся 10к потоков, которые ничего не делают.

А память отъедят, и оно все тормозить начнет, когда она закончится.

Роман Елизаров тут совсем недавно, отвечая на вопрос «короутины Котлина vs виртуальные треды из Loom» сказал, что они предназначены для решения похожих, но все-таки разных задач. Что, мол, Loom нужен для легаси-кода, который редко засыпает, а короутины предназначены для того, чтобы асинхронное программирование на Rx* (где наоборот, потоки очень редко просыпаются) можно было бы переписать более человеко-понятным способом.

www.youtube.com/watch?v=4wM0dfGr3Ec, см. с 1:03:00
А память отъедят, и оно все тормозить начнет, когда она закончится.

в сообщении выше есть сравнение потребления памяти системными потоками и виртуальными.


The kernel threads used 3.7GB/32K = 114KB/thread of heap memory, whilst the Loom virtual threads used 8.4GB/55K = 152KB/thread of heap. The additional heap memory usage for virtual threads appears to be due to the stacks being stored on the heap rather than in kernel memory

При этом дают приличную нагрузку на сборщик мусора.

Если я правильно помню проблема с10к:
а) актуальна была для 1999 года
б) в 2010 была переименована с10м в связи с тем что возникала уже при 10 миллионах соединений
в) воспроизводилась в 99г из-за network socket, а не из-за потоков.

А получится ли эффективно обрабатывать миллион одновременных задач, если каждая лезет на ФС/в БД/куда-то ещё? Я к тому, что вряд-ли все зависимые компоненты, с которым будет работать наше мнимое приложение с виртуальными потоками под капотом, смогут принять эти миллионы. Высока вероятность всё положить. Значит придётся ограничивать количество одновременно обрабатываемых соединений, а тогда уже и особого смысла нет в легковесных нитях, получаются почти те же пулы потоков. А вообще конечно надо дождаться, когда завезут и смотреть уже в каждом конкретном случае.

Да, но для этого нужен неблокирующий API. Например: читай из сокета столько байт, сколько сможешь без блокирования. Тогда все будет прекрасно. Проверено на собственном проекте — нагрузка ограничивалась лишь лимитом на кол-во файловых дескрипторов (~1М)

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

Это будет полезно для вэбсервера, например. На каждый запрос нужно запускать отдельный поток и в таком случае ресурсы машины позволят прожевать намного больше.
Асинхронность как раз стала востребована с ростом популярности распределенных систем, где много сетевых взаимодействий, но нагрузка на каждый узел минимизируется. В распределенных системах «миллион запросов», вовсе не значит, что этот миллион запросов пришел от конечных пользователей, сама система на каждый запрос от пользователя порождает десяток-другой новых запросов, а то и больше. Одна «жирная» задача от пользователя разбивается на десяток более легких и раздается сервисам, нагрузка на каждый сервис уменьшается, но имеем в 10 раз больше сетевого IO. При этом зависимым компонентам не обязательно уметь выдерживать такую же нагрузку, как система в целом

Спасибо за статью.
Есть ли планы когда project loom придет в джаву? Не так давно в планах была java 16-сейчс уже понятно что туда ничего не вошло, а про планы вообще неслышу.
Есть интересная статья в 2 частях от jetty про loom https://webtide.com/do-looms-claims-stack-up-part-1/
Там видно довольно много проблем.
Из более интересных проектов мне видется valhalla.
Проект видно что активно развивается, и прогресс заметен. Фичи приходят в джаву, причем для меня более заметны фичи которые решают целые классы проблем, повышают юзабилити и проивзодительность. Очень интересно было бы почитать статью про его текущее состояние.

1. Про Loom, насколько знаем, Oracle пока что не озвучивали конкретные планы.

2. Про Valhalla чего-то прямо суперсвежего у нас нет, но на прошлогоднем JPoint 2020 был доклад от Сергея Куксенко, над расшифровкой которого мы сейчас работаем, так что можете посмотреть видеозапись или дождаться текстовой версии)
Я изменю тело лямбда-выражения, просто выведу трассировку стека, чтобы вы могли видеть, что на самом деле происходит.

И на скриншоте ниже словили исключение?

Это Thread::dumpStack так выводит стек

Понял.
Благодарю!
На самом деле это не очень критично. По той простой причине, что всё, что сегодня использует мониторы Java, можно механически преобразовать из использования synchronized и wait-notify в использование блокировок из java.util.concurrent.

Вообще-то очень критично. Если изначальная идея была прозрачно для всего кода заменить Thread на VirtualThread, то здесь она заведомо обломается. Большинство IO-кода, используемого сегодня, относится к сторонним библиотекам и фреймворкам, которые модифицировать будет проблематично. И никто не давал гарантий, что очередная библиотека типа jdbc драйвера будет использовать java.util.concurrent вместо synchronized. Насколько я понял из доклада Сергея Куксенко, синхронизации в Loom сильно мешает biased locking, который в скором будет выпилен из java. Но до тех пор Loom остается практически не юзабелен. Возможно вместе с Loom стоило бы ввести автоматические преобразование synchronized в java.util.concurrent в рантайме, пусть даже с регрессиями.


Еще такой момент не освещен — при вытесняющей многозадачности очень важна политика планировщика задач: т.е. при нескольких задачах, ожидающих продолжения, которой из них будет отдаваться приоритет? И дело здесь не может ограничиваеться только значением Thread.priority, ибо разные операции в одном и том же треде могут требовать разного приоритета.

>Большинство IO-кода, используемого сегодня, относится к сторонним библиотекам и фреймворкам, которые модифицировать будет проблематично.

Хороший вопрос, только я вот не уловил одной детали: как в чужом коде заменить Thread виртуальным? Да еще прозрачно? Да, и драйвер JDBC — это еще не самый плохой случай, возможно (хотя закрытый код от условного MS не даст вам его модифицировать, конечно). Но он хотя бы достаточно маленький (для декомпиляции, скажем).

Есть еще фреймворки и проекты, которые в типичном случае намного больше, чем тот бизнес-код, который на их основе пишется. Ну так, условно — я могу написать на спарке программку из трех строк, и она будет вполне работоспособна и полезна, но при этом изменения в сам спарк внести — это то еще развлечение, даже просто из-за размера кодовой базы и числа зависимостей.

Это как раз не проблема. Thread вы заменяете на файбер (оригинальное более удачное название виртуального треда) в своем коде, который уже дергает всякие IO-библиотечки типа JDBC и пр., а те в свою очередь не догадываются в каком треде работают. Если же у вас какой-нибудь фреймворк со встроенным сервером, или сторонний Executor, то практически везде можно указать свой ThreadFactory, который будет создавать файберы вместо тредов.


Да, и драйвер JDBC — это еще не самый плохой случай

… но не в случае, например, с Oracle JDBC...

>не догадываются в каком треде работают.
Э, если бы они просто догадывались. А сами-то они будут потоки создавать, и далеко не всегда можно фабрику указать — это вам наверное везло, что можно было. Ну в общем я это к тому, что эту возможность вам тоже не гарантирует никто.
А сами-то они будут потоки создавать

И пусть создают, если нужно. Задача не избавиться на 100% от всех потоков, а увеличить масштабируемость приложения. Для этого нужно заменить потоки лишь в основном пуле воркеров, который растет линейно от количества параллельных реквестов. При этом ничего плохого нет, если например jdbc datasource запустит свой отдельный нативный тред, который время от времени собирает незакрытые коннекшны и операторы — он будет один на всё приложение и никак не зависит от количества реквестов. Если же ваша библиотека на каждый вызов открывает дополнительно еще один тред — смело можете от нее избавляться уже сейчас.

>смело можете от нее избавляться уже сейчас.
Ну это перебор :) Есть вполне себе нередкие случаи, когда это нормально.

Я в целом согласен — если потоков где-то в потрохах создается неизвестное число — это сомнительный код сам по себе. Но с другой стороны — вот вы свое приложение смасштабировали, и у вас теперь 1000 файберов. Или лучше миллион. Вы же считаете, что они легкие и дешевые.

Но если при этом в каждом из файберов какой-то вызов чужой библиотеки создаст всего лишь еще 1 обычный поток — это уже порушит всю вашу оптимизацию, а масштаб разрушений вы при этом зададите сами. Ну т.е. при такой миграции всегда скорее всего придется изучить, а что именно чужая библиотека делает там у себя в потрохах с потоками. При этом просто где-то в сигнатуре этого ничего естественно может не быть видно.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий