Project loom: что повышает производительность при использовании виртуальных потоков?
Чтобы дать некоторый контекст, я уже некоторое время слежу за ткацким станком. Я прочитал состояние ткацкого станка. Я занимался асинхронным программированием.
Асинхронное программирование (предоставляемое java nio) возвращает поток в пул потоков, когда задача ожидает, и делает все возможное, чтобы не блокировать потоки. И это дает большой прирост производительности, теперь мы можем обрабатывать гораздо больше запросов, поскольку они напрямую не связаны количеством потоков ОС. Но здесь мы теряем контекст. Та же задача теперь НЕ связана только с одним потоком. Весь контекст теряется, когда мы отделяем задачи от потоков. Трассы исключений не предоставляют очень полезной информации, и отладка затруднена.
В комплекте ткацкий станок с virtual threads
которые становятся единой единицей параллелизма. И теперь вы можете выполнять одну задачу на одномvirtual thread
.
До сих пор все в порядке, но в статье говорится, что проект loom:
Простой синхронный веб-сервер сможет обрабатывать гораздо больше запросов, не требуя дополнительного оборудования.
Я не понимаю, как мы получаем преимущества в производительности с помощью ткацкого станка проекта по сравнению с асинхронными API? Вasynchrounous APIs
убедитесь, что ни один поток не простаивает. Итак, что делает проектный ткацкий станок, чтобы сделать его более эффективным и производительным,asynchronous
API?
РЕДАКТИРОВАТЬ
Позвольте перефразировать вопрос. Допустим, у нас есть http-сервер, который принимает запросы и выполняет некоторые грубые операции с резервной постоянной базой данных. Скажем, этот http-сервер обрабатывает много запросов - 100 тысяч об / мин. Два способа реализовать это:
- HTTP-сервер имеет выделенный пул потоков. Когда поступает запрос, поток переносит задачу до тех пор, пока не достигнет БД, при этом задача должна ждать ответа от БД. На этом этапе поток возвращается в пул потоков и продолжает выполнять другие задачи. Когда DB отвечает, он снова обрабатывается каким-то потоком из пула потоков и возвращает HTTP-ответ.
- HTTP-сервер просто появляется
virtual threads
для каждого запроса. Если есть ввод-вывод, виртуальный поток просто ждет завершения задачи. А затем возвращает HTTP-ответ. По сути, дляvirtual threads
.
Учитывая, что оборудование и пропускная способность останутся прежними, будет ли какое-то одно решение лучше другого с точки зрения времени отклика или обработки большей пропускной способности?
Думаю, разницы в производительности не будет.
3 ответа
Мы не получаем преимущества перед асинхронным API. Потенциально мы получим производительность, аналогичную асинхронной, но с синхронным кодом.
Ответ @talex говорит об этом четко. Добавляя к нему дальше.
Loom больше похож на нативную абстракцию параллелизма, которая дополнительно помогает писать асинхронный код. Учитывая его абстракцию уровня виртуальной машины, а не просто уровень кода (например, то, что мы делали до сих пор с и т. д.), он позволяет реализовать асинхронное поведение, но с уменьшением шаблона.
С Loom спасает более мощная абстракция . Мы неоднократно видели, как абстракция синтаксическим сахаром позволяет эффективно писать программы. Будь то FunctionalInterfaces в JDK8, for-comprehensions в Scala.
С ткацким станком нет необходимости связывать несколько CompletableFuture (для экономии ресурсов). Но можно написать код синхронно. И с каждой обнаруженной блокирующей операцией (ReentrantLock, ввод-вывод, вызовы JDBC) виртуальный поток приостанавливается. И поскольку это легкие потоки, переключение контекста намного дешевле, чем потоки ядра.
При блокировке фактический поток-носитель (который выполнял
run
-тело виртуального потока), задействуется для выполнения другого запуска виртуального потока. Таким образом, поток-носитель не сидит без дела, а выполняет какую-то другую работу. И возвращается, чтобы продолжить выполнение исходного виртуального потока всякий раз, когда он не припаркован. Точно так же, как будет работать пул потоков. Но здесь у вас есть один поток-носитель, выполняющий тело нескольких виртуальных потоков, переключаясь с одного на другой при блокировке.
Мы получаем то же поведение (и, следовательно, производительность), что и написанный вручную асинхронный код, но вместо этого избегаем стандартного шаблона, чтобы делать то же самое.
Рассмотрим случай веб-фреймворка, где есть отдельный пул потоков для обработки ввода-вывода, а другой — для выполнения http-запросов. Для простых HTTP-запросов можно обслуживать запрос из самого потока http-пула. Но если есть какие-либо блокирующие (или) высокопроизводительные операции ЦП, мы позволяем этой активности выполняться в отдельном потоке асинхронно.
Этот поток будет собирать информацию из входящего запроса, создавать
CompletableFuture
, и свяжите его с конвейером (чтение из базы данных как один этап, за которым следуют вычисления из него, а затем еще один этап для обратной записи в базу данных, вызовы веб-службы и т. д.). Каждый из них является этапом, и в результате
CompletablFuture
возвращается обратно в веб-фреймворк.
Когда результирующее будущее завершено, веб-фреймворк использует результаты для передачи обратно клиенту. Вот как
Play-Framework
и другие, занимались этим. Обеспечение изоляции между пулом обработки http-потоков и выполнением каждого запроса. Но если мы углубимся в это, почему мы это делаем?
Одной из основных причин является эффективное использование ресурсов. В частности, блокировка звонков. И поэтому мы связываемся с
thenApply
и т. д., чтобы ни один поток не блокировался ни при каких действиях, и мы делали больше с меньшим количеством потоков.
Это прекрасно работает, но довольно многословно . И отладка действительно болезненна, и если один из промежуточных этапов приводит к исключению, поток управления выходит из строя, что приводит к дальнейшему коду для его обработки.
С Loom мы пишем синхронный код и позволяем кому-то другому решать, что делать в случае блокировки. Вместо того, чтобы спать и ничего не делать.
На http-сервере есть выделенный пул потоков.... Насколько велик пул? (Количество процессоров)*N + C? N>1 можно вернуться к анти-масштабированию, поскольку конкуренция за блокировку увеличивает задержку; где N=1 может недоиспользовать доступную полосу пропускания. Существует хороший анализ здесь.
HTTP-сервер просто появляется... Это было бы очень наивной реализацией этой концепции. Более реалистичный подход будет стремиться к сбору из динамического пула, который сохранял бы один реальный поток для каждого заблокированного системного вызова + по одному для каждого реального процессора. По крайней мере, это то, что придумали разработчики Go.
Суть в том, чтобы уберечь {обработчики, обратные вызовы, завершения, виртуальные потоки, горутины: все PEA в модуле} от борьбы за внутренние ресурсы; таким образом, они не полагаются на системные механизмы блокировки до тех пор, пока это не станет абсолютно необходимым. Это относится к предотвращению блокировки и может быть выполнено с помощью различных стратегий организации очередей (см. libdispatch) и т. д. Обратите внимание, что это оставляет PEA отделенным от основного системного потока., потому что они внутренне мультиплексированы между собой. Это ваша забота о разводе концепций. На практике вы передаете абстракцию указателя контекста на свои любимые языки.
Как указывает 1, есть ощутимые результаты, которые можно напрямую связать с этим подходом; и несколько нематериальных активов. Блокировка проста - вы просто устанавливаете одну большую блокировку для своих транзакций, и все готово. Это не масштабируется; но мелкозернистая блокировка сложна. Трудно приступить к работе, сложно выбрать крупность зерна. Когда использовать {блокировки, резюме, семафоры, барьеры, ... } очевидно в примерах из учебников; немного меньше в глубоко вложенной логике. Избегание блокировок по большей части устраняет это и ограничивает конкурирующие листовые компоненты, такие как malloc().
Я сохраняю некоторый скептицизм, поскольку исследования обычно показывают плохо масштабируемую систему, которая преобразуется в модель предотвращения блокировок, а затем оказывается лучше. Мне еще предстоит увидеть такой, который позволил бы некоторым опытным разработчикам проанализировать поведение синхронизации системы, преобразовать его для масштабируемости, а затем измерить результат. Но даже если бы это была победа, опытные разработчики - редкий (иш) и дорогой товар; в основе масштабируемости лежат финансовые вопросы.