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

Помимо Loom, которому посвящена эта статья, вам следует обратить внимание на Valhalla, который в некоторых случаях может удвоить производительность Java, и Graal , который делает так много всего, что я даже не знаю, с чего начать! И, конечно же, язык стал менее подробным благодаря Amber.

Эти проекты могут даже изменить восприятие других языков, поэтому они потенциально могут иметь очень большое влияние.

Например: Loom + Graal предоставляют вам продолжения (совместные подпрограммы) и предварительную компиляцию, что делает Go менее привлекательным, чем сейчас.

Loom + Amber дает вам волокна (позволяющие потенциально более простые системы акторов) и более короткий синтаксис, что делает Scala менее привлекательной, чем сейчас. Valhalla + Graal может сократить разрыв в производительности с C ++. И Graal может подтолкнуть Python к запуску в самой JVM, или, по крайней мере, PySpark может извлечь из этого выгоду.

Но давайте сосредоточимся на Loom. И поскольку в настоящее время об этом не так много практической информации, мы продолжим, создадим и будем использовать эту экспериментальную JVM и проведем несколько тестов. Пусть говорят цифры!

Project Loom

Раньше у Java были зеленые потоки, по крайней мере, в Solaris, но современные версии Java используют собственные потоки. Нативные потоки хороши, но относительно тяжелы, и вам может потребоваться настроить ОС, если вы хотите, чтобы их были десятки тысяч.

Project Loom вводит продолжения (совместные подпрограммы) и волокна (тип зеленых нитей), позволяя вам выбирать между нитями и волокнами. С помощью Loom даже ноутбук может легко работать с миллионами волокон, открывая дверь новым или не таким новым парадигмам.

Небольшое отступление: Erlang

Возможно, вы слышали об Erlang. Это очень интересный язык, намного старше Java, с некоторыми недостатками, но также с некоторыми впечатляющими функциями. Erlang имеет встроенную поддержку зеленых потоков, и фактически виртуальная машина подсчитывает операции и время от времени переключается между зелеными потоками.

В Erlang программа обычно имеет много долгоживущих, не очень загруженных потоков. На самом деле ожидается, что он будет обслуживать каждого пользователя с выделенным потоком. Многие из этих потоков могут выполнять сетевые операции (ведь Erlang был разработан Ericsson для телекоммуникационной отрасли), и эти сетевые операции являются синхронными. Да, синхронно. Мы можем обслуживать миллион пользователей на одной машине с большим объемом оперативной памяти, используя простые синхронные сетевые операции.

Синхронный против асинхронного

В течение многих лет нам говорили, что масштабируемые серверы требуют асинхронных операций, но это не совсем так.

Конечно, если вам нужно масштабировать с помощью пула потоков (или даже одного потока), у вас практически нет альтернатив: вам нужно использовать асинхронные операции. А асинхронные операции можно очень хорошо масштабировать.

Когда я пришел в Opera Software в 2008 году, я был немного удивлен, узнав, что Presto, ядро ​​браузера, является однопоточным. Да, один единственный поток. Но этого было достаточно. Десятки вкладок, отображающих HTML и обрабатывающих Javascript, загрузки из сети, файловые операции, файлы cookie, кеш и т. Д. И только один поток, много асинхронных операций и повсюду обратных вызовов. И это сработало очень хорошо.

Но асинхронный код - это сложно. Это может быть очень сложно. Асинхронные вызовы прерывают поток операций, и то, что может быть всего лишь 20 строками простого кода, может потребоваться разделить на несколько файлов, выполнить по потокам, и разработчикам может потребоваться несколько часов, чтобы выяснить, что на самом деле происходит.

Разве не было бы неплохо получить простоту синхронных операций с производительностью асинхронных вызовов?

Волокна спешат на помощь

Ткацкий станок вводит волокна. Это здорово, но этого недостаточно. Чтобы делать полезные вещи, вам понадобится сетевой стек, совместимый с оптоволоконным кабелем. Когда я попробовал Loom несколько месяцев назад, этого не произошло. Создание около 40–50 волокон было достаточно, чтобы в сети появились ошибки. Проект был слишком незрелым.

В июне JDK 13 принял в основном JEP 353 (https://openjdk.java.net/jeps/353), который переписал Java Socket API, чтобы он был совместим с Fiber.

Хотя не все работает, теперь Loom можно использовать для сетевых операций.

Пришло время создать актерскую систему, которая могла бы использовать волокна Loom.

Хорошо, может быть, это немного рано, так как Project Loom все еще экспериментальный, а JDK 13 должен появиться в сентябре, но я не удержался, и я создал и открыл исходный код небольшой системы актеров, способной использовать Loom: Fibry. Мы будем использовать его для тестирования Loom и увидим, действительно ли волокна лучше нитей.

Актеры и Фибри

Акторы используются в многопоточной среде для относительно простого достижения параллелизма. В частности, субъекты являются однопоточными, поэтому по определению у вас нет проблемы с параллелизмом, если они работают только со своим состоянием; вы можете изменить состояние актера, отправляющего ему сообщения.

Erlang обеспечивает эту безопасность, имея только константы (без циклов for, и вы даже не можете переключать две переменные традиционным способом…), а Java - нет. Но актеры по-прежнему могут быть очень полезными.

Отличный вариант использования для субъектов - это когда у вас есть длительная задача, которая особенно легка, обычно потому, что она полагается на сетевые операции и просто ждет, пока клиенты что-то сделают. Например, в сети IoT все устройства могут быть постоянно подключены к серверу управления, отправляя сообщения только время от времени. Чат - еще один пример программы, в которой актеры могут извлечь выгоду. И сервер, поддерживающий WebSockets, может быть другим кандидатом.

Fibry - это моя актерская система, небольшая, гибкая и простая в использовании, и, конечно же, использующая преимущества Loom. Fibry работает с любой версией Java, начиная с Java 8, и не имеет зависимостей, но требует, чтобы Loom использовал волокна. Если вы хотите получить дополнительную информацию, посетите GitHub: https://github.com/lucav76/Fibry/.

Строительный ткацкий станок

Сборка Loom занимает немного времени, но легко. Вы можете получить некоторую информацию здесь:

Https://wiki.openjdk.java.net/display/loom/Main#Main-DownloadandBuildfromSource

После установки Mercurial (OpenJDK все еще находится на Mercurial). Вам необходимо выполнить следующие команды:

hg clone http://hg.openjdk.java.net/loom/loom
cd loom
hg update -r fibers
sh configure
make images

Возможно, вам потребуется установить некоторые пакеты во время процесса, но ‘sh configure’ должен указать вам, какие команды следует запускать.

Вот и все!

Теперь вы можете создать «Hello World» с помощью волокон:

var fiber = FiberScope.background().schedule(() -> System.out.println(“Hello World”));

Вы можете получить более подробную информацию здесь:

Https://wiki.openjdk.java.net/display/loom/Structured+Concurrency

Мы не собираемся использовать волокна напрямую, но мы будем использовать Fibry, поскольку нас в первую очередь интересует, как акторы могут извлечь из них пользу.

Сравнение волокон и нитей

Давайте посчитаем, сколько времени нам нужно на создание (и поддержание активности) 3К потоков. Вы можете попробовать большее число, если ваша ОС настроена правильно. Я использую стандартную конфигурацию виртуальной машины c5.2xlarge с Loom JDK без параметров. Он может создавать потоки 3K, но не 4K.

Когда вы запускаете этот тест с большим количеством потоков, будьте готовы к тому, что это может быть немного сложно для вашего ПК, и вам может потребоваться перезагрузка.

for(int i=0; i<3000; i++)
    Stereotypes.threads().sink(null);

Этот код создает 3К «потоков-приемников», которые просто отбрасывают полученные сообщения. В моей виртуальной машине для выполнения требуется 210 мс.

Давайте попробуем создать 1 миллион волокон, используя метод fiber () вместо thread ():

for(int i=0; i<1_000_000; i++)
    Stereotypes.fibers().sink(null);

В моей виртуальной машине я могу создавать волокна 3M. 3 миллиона!

С помощью Loom можно создать в 1000 раз больше волокон, чем нитей! Вы, безусловно, можете настроить виртуальную машину и ОС для увеличения количества потоков, но, насколько я понимаю, существует ограничение около 32 КБ.

Волокна также создаются намного быстрее. Для 3K потоков требуется 210 мс, но за то же время можно создать 200K волокон, а это означает, что создание волокон происходит до 70 раз быстрее, чем создание потоков!

Измерение переключения контекста

Как правило, компьютеру необходимо переключаться с одного потока на другой, и это занимает небольшое, но значительное время. Теперь мы попытаемся увидеть, быстрее ли волокна справляются с этой конкретной проблемой. Чтобы попытаться измерить переключение контекста, мы создадим два потока и будем обмениваться сообщениями синхронно с кодом, подобным этому (вам нужно вызвать ActorSystem.setDefaultStrategy (), чтобы выбрать потоки или волокна):

var act = ActorSystem.anonymous().newActorWithReturn((Integer n) -> n * n);
Stereotypes.def().runOnceSilent(() -> {
    for (int i = 0; i < 250_000; i++)
        act.sendMessageReturn(i).get();
}).closeOnExit(actorAnswer).waitForExit();

Здесь у нас есть актер act, способный вернуть квадрат числа, а другой актер просит его сделать это 250 тысяч раз, ожидая результата.

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

Сетевые операции

Давайте теперь проверим, все ли в порядке с сетевыми операциями.

Ниже приведен простой код HTTP HelloWorld, запускающий встроенный HTTP-сервер Java:

Stereotypes.def().embeddedHttpServer(12345, exchange -> “Hello World!”);

Каждый раз, когда подключается новый клиент, создается новый субъект для обработки запроса. В этом случае потоки и волокна работают примерно одинаково - примерно 2200 запросов в секунду. Здесь узким местом, вероятно, является встроенный HTTP-сервер, который не предназначен для нагрузки на сервер.

Итак, давайте попробуем написать супер простой HTTP-сервер, который всегда будет отвечать одной и той же строкой:

Stereotypes.def().tcpAcceptorSilent(12345, conn -> {
    try ( var is = conn.getInputStream(); 
          var os = conn.getOutputStream()) {
        while (is.read() != '\n' || is.read() != '\r' || 
               is.read() != '\n') { /** Skip to the end */ }
        os.write("HTTP/1.1 200 OK\r\n" + 
                 "Content-Length: 6\r\n\r\nHello!".getBytes());
    }
}, null).waitForExit();

Я тестирую Apache Bench, используя 100 потоков:

ab -k -n 50000 -c 100 http://localhost:12345/

Версия с потоком может обслуживать почти 11 КБ запросов в секунду, тогда как волокна оценивают более 24 КБ. Таким образом, в этом тесте волокна в два раза быстрее, чем потоки.

Волокна всегда быстрее?

Не совсем. По какой-то причине потоки кажутся немного быстрее при отправке асинхронных сообщений - около 8,5 МБ в секунду, в то время как пиковая скорость волокон составляет около 7,5 МБ в секунду. Кроме того, в этом конкретном тесте потоки, похоже, меньше страдают от перегрузки при увеличении количества потоков.

Это могло быть решаемым переключением на другую систему обмена сообщениями, отличную от той, которую использует Fibry. Кроме того, давайте не будем забывать, что Loom еще не готов к производству, поэтому еще есть запас для улучшения поведения.

Если вы хотите провести тесты самостоятельно, вы можете найти полный код и еще несколько тестов здесь: https://github.com/lucav76/FibryBench/

Выводы

Кажется, Ткацкий станок в хорошей форме. Волокна ведут себя очень хорошо с точки зрения производительности и могут значительно увеличить емкость сервера, в то же время упрощая код. Волокна могут быть решением не для всех проблем, но, безусловно, системы-участники могут извлечь из них большую пользу.

Я с нетерпением жду, когда Loom объединится с основной веткой OpenJDK. А вы?