Хабрахабр

Раздача халявы: нетормозящие треды в Java. Project Loom

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

Налетай! Объясняем работу Project Loom на коробках с пиццей!

Состав поставки:

  • Видеокаст (основная часть). Для тех, кто любит потреблять видео.
  • Полная текстовая расшифровка статьи. Там есть ссылки!

Всё это снимается и пишется специально для Хабра.

Блогер может сделать за лайки практически всё, что угодно. Мы живём в жестоком новом мире, где лайки стоят больше, чем деньги. Финальная стадия международного капитализма и технологической распущенности.

Неизвестно, какие мерзости вы вообще делаете, чтобы заработать. Знаю, что вам ваши лайчики дались нелегким трудом. Я не хочу об этом знать вообще. Возможно, отвечаете на комментарии, которые вам на самом деле не нравятся, людям, которых вы ненавидите, и так зарабатываете себе вожделенные плюсики. Просто отслюнявьте мне немножко лайчиков, и я сделаю вид, что не знаю, откуда они. Лайки не пахнут. Это всё, что имеет значение для продажного блогера типа меня.

Спасибо. Все лайчонки пойдут на создание нового контента.

Привет, джаваны, Олег на связи.

Часто ли вы видите у себя на веб-сервисе вот такую картинку: вначале все было хорошо, потом к вам пришёл миллион китайцев, сервис наделал миллион тредов и захлебнулся к чертям собачьим?

Хотите ли вы вот такую няшную картинку?

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

Помните, как Wylsacom распаковывал айфоны? Мы будем заниматься, по сути, распаковкой нового фреймворка. Потому что Хабр — оплот мейнстрима, а проплаченные видосы — это, извините, лучи поноса. Кое-кто уже и не помнит старых обзорщиков, а всё почему? В этом посте мы будем заниматься исключительно техническим хардкором.

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

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

Мы постоянно разговариваем о корутинах в Kotlin. Но есть еще одна бонусная цель. Скоро будет интервью с Андреем Бреславом — который отец Kotlin. Вот недавно были интервью с Ромой Елизаровым, богом корутин, и с Пашей Финкельштейном, который на них собирается писать бэкенды. И если вы не знаете, что такое Loom, вам при чтении этих интервью вам может стать стрёмно. И везде так или иначе упоминается проект Loom, потому что это аналог корутин. А есть ты, и ты не с ними, ты чмо. Вот есть какие-то клевые чуваки, они обсуждают крутые вещи. Это очень тупо.

Не делайте так, прочитайте, что такое Loom, в этой статье, или дальше смотрите это видео, я все объясню.

Есть такой чувак, Рон Пресслер. Итак, в чем завязка.

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

Ну и поймите правильно, я такой же. Есть чертово количество говнокодеров, которые ничего не делают, просто говорят. Что улучшить? Или вот есть люди, которые вроде бы клевые инженеры, но вообще в несознанке, они такие говорят: «В джаве надо улучшить треды». Что — треды?

Людям вообще лень думать.

Как там в анекдоте:
Летят в самолете Петька и Василий Иванович.
Василий Иванович спрашивает: — Петька, приборы?
Петька отвечает: — 200!
Василий Иванович: — А что 200?
Петька: — А что приборы?

Я был этой весной на Украине, мы летели из Беларуси (вы понимаете, почему напрямую из Питера нельзя). Расскажу историю. Таможенники весьма приятные, на полном серьезе спрашивали, является ли Java устаревшей технологией. И на таможне мы сидели что-то часа два, не меньше. И я же типа докладчик, должен понтануться, стою и, как положено, бесстыже рассказываю о вещах, которые вообще не использую. Рядом сидели люди, которые летят на ту же конфу. И по пути рассказал о дистрибутиве JDK под названием Liberica, это такой JDK для Raspberry Pi.

Не проходит и полгода, как ко мне в телегу стучится чел и говорит, что вот, гляди, мы запилили на Либерике решение в прод, и у меня уже есть доклад про это на белорусскую конфу jfuture.by. И что вы думаете. Это не евангелист какой-то паршивый, а норм чувак, норм инженер. Вот это подход.

Ну и ещё куча крутых видных экспертов, налетай! Кстати, у нас скоро будет конференция Joker 2018, на которой будут и Андрей Бреслав (очевидно, шарящий в корутинах), и Паша Финкельштейн, а о поддержке Loom в Spring можно будет спросить у Джоша Лонга.

Люди пытаются сквозь свои деграднувшие два нейрона мысль провести, такие наматывают сопли на кулак, и бормочут: "В джаве треды не так, в джаве треды не так". И вот, возвращаясь к тредам. Что приборы? Приборы! Это вообще ад.

А через год пилит рабочую демку. И вот приходит Пресслер, нормальный не деграднувший чувак, и вначале делает вменяемое описание. А демка — это вообще космос. Все это я говорил, чтобы вы поняли, что нормальное описание проблем, нормальная документация — это такой героизм особого рода. Ему больше всего надо. Это первый человек, который вообще что-то сделал в этом направлении.

Вместе с демкой Пресслер выступил на конференции и выпустил вот такое видео:

Я совершенно не претендую на уникальность этого материала, всё, что есть в этой статье, придумал Рон. По сути, вся эта статья — это некий обзор на сказанное там.

Обсуждение идёт по поводу трех болезненных тем:

  • Continations
  • Fibers
  • Tail-calls

Вероятно, его так задолбало пилить Квазар и бороться с его глюками, что вот сил нет — надо пихать это в рантайм.

Некоторые уже потеряли надежду, что мы когда-то увидим демку, но месяц назад её-таки родили и показали, что видно по этому твиту. Было это год назад, и с тех пор они пилили прототип.

Ну да, tail calls они пока не осилили, но хотят. Все три болезненные темы в этой демке имеются или прямо в коде, или хотя бы присутствуют морально.

На одном стуле встроены пики, на другом — растут цветы. Несчастные пользователи, прикладные разработчики, когда делают API, принуждены выбирать между двумя стульями. И ни то, ни другое нам не подходит.

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

(Ну и да, это как-то связано с тем, что сокеты в джаве устроены дебильно, но это тема другого разговора)

Представьте, вы пишете какую-нибудь MMO.

И пилот, понятно, — это сложная бизнес-логика, а не выдача какого-нибудь HTML, который можно руками поразлинеивать в лупе. Например, во время Северной Войны в EVE Online в одной точке пространства собрались две тысячи четыреста пилотов, каждый из которых — условно, будь это написано на Java, — был бы не один тред, а несколько.

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

Но зато пример наглядный и с картинками. Хотя, я, наверное, зря привожу в пример именно EVE, потому что, насколько понимаю, у них всё написано на Python, а в Python с многопоточностью всё ещё хуже, чем у нас — и можно считать плохой конкарренси фичей языка.

Если вас заинтересовала тематика ММО вообще и история «Северной Войны» в частности, недавно на канале БУЛДЖАТь (что бы ни значило это название) появился очень хороший ролик на эту тему, смотреть с моей временной метки.

Возвращаемся к теме.

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

Я поинтересовался у эксперта, известного академика Эскобара, что он думает по этому поводу:

На помощь спешат так называемые файберы. Что же делать?

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

В результате повышается утилизация железа, и мы используем меньше серверов в кластере на ту же самую задачу. Файберы могут на себе реализовать плюсы как синхронного, так и асинхронного программирования. Бабосы. Ну и в карман за это получаем лавандосики. Денежки. Лавэ. За сэкономленные сервера. Ну, вы поняли.

Люди не понимают разницы между континуациями и файберами.
Сейчас будет Культпросвет! Первое, что хочется обсудить.

Огласим факт: Continuation и Fiber — это разные вещи.

Файберы построены поверх механики под названием Continuations.

Его иногда можно даже склонировать или сериализовать, даже в тот момент, пока он спит. Continuations (если точнее, delimited continuations) — это некое вычисление, исполнение, кусок программы, который может заснуть, потом проснуться и продолжить выполнение с того места, как заснул.

Используя нормальную русскую терминологию, можно легко прийти к ситуации, когда разница между русским и английским термином становится слишком большой и никто больше не понимает смысла сказанного. Я буду использовать слово «континуация», а не «продолжение» (как это написано на Википедии), потому что все мы общаемся на рунглише.

Просто слово «yield» — оно какое-то уж совсем мерзковатое. Ещё я иногда буду использовать слово «вытеснение» вместо английского варианта «yield». Поэтому будет «вытеснение».

Очень важно, что никакого конкарренси внутри континуации не должно быть. Так вот. Она сама по себе — минимальный примитив этого процесса.

Именно внутри и напрямую, потому что многозадачность у нас кооперативная. Можно думать о континуации как о Runnable, у которого внутри можно вызвать метод pause(). Такая магия. И потом можно запустить его ещё раз, и вместо того, чтобы всё считать заново, он продолжит с места, где остановился. К магии мы еще вернемся.

Сейчас поговорим о том, что там есть. Где взять демку с работающими континуациями — мы обсудим в самом конце.

(src/java.base/share/classes/java/lang/Continuation.java). Сам класс континуации лежит по адресу в java.base, все ссылки будут в описании. Но этот класс очень большой, объемный, поэтому имеет смысл посмотреть только на какую-то выжимку из него.

public class Continuation implements Runnable
}

Например, по состоянию на предыдущий день континуация не реализовывала интерфейс Runnable. Заметьте, что на самом деле файл этот постоянно меняется. Относитесь к этому как к некой зарисовке.

body — это код, который вы пытаетесь запускать, а scope — это некий скоп, позволяющий вкладывать континуации в континуации. Взгляните на конструктор.

Можно спросить с помощью метода isDone, завершилось ли всё до конца. Соответственно, можно или заранить этот код до конца методом run, или вытеснить его с каким-то конкретным скопом с помощью метода yield (скоп тут нужен для чего-то типа пробрасывания эксепшенов по вложенным обработчикам, но нам это неважно как пользователям).

Например, если внутри континуации у нас случился переход в нативный код и на стеке появился нативный фрейм, то зайилдить нельзя. И по причинам, продиктованным исключительно нуждами текущей реализации (но скорей всего, в релиз тоже попадёт), совершенно не всегда можно сделать yield. По умолчанию, при попытке зайилдить такое, выбрасывается исключение… но файберы, построенные поверх континуаций, перегружают этот метод и делают кое-что другое. Ещё так произойдет, если попытаться вытесниться, пока внутри тела континуации взят нативный монитор, типа синхронизованного метода. Об этом будет чуть позже.

Использовать это можно примерно следующим образом:

Continuation cont = new Continuation(SCOPE, () -> { while (true) { System.out.println("before"); Continuation.yield(SCOPE); System.out.println("after"); } }); while (!cont.isDone()) { cont.run();
}

Опять, это не «всамделишный» код, это какая-то зарисовка. Это пример из презентации Пресслера.

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

Оно предназначено для создателей системных фреймворков. Но вообще, не предполагается, что обычные прикладные программисты будут касаться этого API. Вот увидите. Системообразующие фреймворки вроде Spring Framework сразу же заадоптят этот фичу, как только она выйдет. Такое, лайтовое предсказание, потому что здесь всё довольно очевидно. Считайте это за предсказание. Это слишком важная фича, чтобы ее не заадоптить. Все данные для предсказания есть. Ну а если вы — разработчик Spring, то знали, на что шли. Поэтому не нужно заранее беспокоиться, что кто-то вас будет истязать кодированием вот в таком виде.

И вот уже поверх континуаций построены файберы.

Итак, что в нашем случае означают файберы.

Это некая абстракция, представляющая из себя:

  • Легкие треды, обрабатываемые в самой JVM, а не в операционной системе;
  • С крайне низкими оверхедами на создание, поддержание жизни, переключение задач;
  • Которые можно запускать миллионы раз.

Например, в Kotlin есть корутины, реализованные на очень умной генерации байткода. Многие технологии пытаются сделать файберы тем или иным образом. Но рантайм — более правильное место для реализации подобных вещей. ОЧЕНЬ УМНОЙ.

Можно использовать асинхронные API, но это вряд ли стоит называть «упрощением»: даже использование таких штук, как Reactor, Spring Project Reactor, позволяющих писать вроде-бы-линейный код, не особо поможет при необходимости отладки сложных проблем. Как минимум, JVM уже умеет хорошо справляться с тредами, а всё, что нам нужно — это упростить процесс кодирования многопоточности.

Итак, файбер.

Это: Файбер состоит из двух компонентов.

  • Continuation (континуейшен)
  • Scheduler (скедьюлер)

То есть:

  • Континуация
  • Планировщик

Я думаю, планировщик тут именно Джей. Можете сами решить, кто здесь планировщик.

  • Файбер оборачивает код, который вы хотите исполнить, в континуации
  • Планировщик запускает их на пуле из carrier threads

Буду называть их тредами-носителями.

Executor, а встроенный планировщик — ForkJoinPool. В текущем прототипе используется java.util.concurrent. В будущем там может появиться что-то поумней, но пока вот так. Всё у нас есть.

Как ведет себя континуация:

  • Вытесняется (yield), когда происходит блокировка (например, на IO);
  • Продолжается, когда готова продолжиться (например, IO-операция завершилась и можно двигаться дальше).

Текущий статус работ:

  • Основной фокус на философии, концепциях;
  • API не зафиксировано, оно есть «для галочки». Это исследовательский прототип;
  • Есть готовый закодированный работающий прототип класса java.lang.Fiber.

О нем пойдет речь.

Что уже запилили в файбер:

  • В нем работает запуск задач;
  • Паркинг-анпаркинг на тред-носитель;
  • Ожидание завершения файбера.

mount();
try { cont.run();
} finally () { unmount();
}

  • Мы можем смонтировать файбер на тред-носитель;
  • Потом запустить континуацию;
  • И ждать, пока она не вытеснится или честно не остановится;
  • В конце концов, мы всегда уходим с треда.

Этот псевдокод выполнится на планировщике ForkJoinPool или на каком-то другом (который в конце концов окажется в финальной версии).

Fiber f = Fiber.execute( () -> { System.out.println("Good Morning!"); readLock.lock(); try { System.out.println("Good Afternoon"); } finally { readLock.unlock(); } System.out.println("Good Night");
});

Глядите, мы создаем файбер, в котором:

  • приветствуем всех;
  • блочимся на РЕЕНТРАНТ ЛОКЕ;
  • по возвращении поздравляем с обедом;
  • в конце концов отпускаем лок;
  • и прощаемся.

Всё очень просто.

Сам Project Loom знает, что при срабатывании readLock.lock(); ему стоит вмешаться и неявно сделать вытеснение. Мы не вызываем вытеснение напрямую. Пользователь этого не видит, но оно там происходит.

Давайте на примере стека с пиццей продемонстрируем, что происходит.

Вначале тред-носитель находится в состоянии ожидания, и ничего не происходит.

Вершина стека наверху, напоминаю.

Потом файбер запланировали к исполнению, и таск файбера начал запускаться

Внутри себя он, очевидно, запускает континуацию, в которой уже находится настоящий код.

С точки зрения пользователя, мы здесь еще ничего не запустили.

Вот только первый фрейм юзерского кода появился на стеке, и он отмечен фиолетовым.

Дальше код выполняется-выполняется, в какой-то момент таск пытается захватить лок и заблочиться на нем, что приводит к автоматическому вытеснению.

И исчезает. Всё, что есть на стеке континуации, сохраняется в некое магическое место.

А это — окончание кода файбера. Как видим, поток возвращается в файбер, на инструкцию, которая идет следом за Continuation.run.

Таск файбера заканчивается, тред-носитель ждет новой работы.

Файбер запаркован, где-то лежит, континуация полностью вытеснена.

Таск этого файбера запускается снова. Рано или поздно наступает момент, когда тот, кто владеет локом, отпускает его.
Это приводит к тому, что файбер, который ждал отпускания лока, анпаркится.

  • ReentrantLock.unlock
  • LockSupport.unpark
  • Fiber.unpark
  • ForkJoinPool.execute

И мы быстро возвращаемся к стеку, который был недавно.

И в этом смысл! Причем тред-носитель может быть совершенно другой.

Снова запускаем континуацию.

Стек восстанавливается, и выполнение продолжается с инструкции после Continuation.yield. И тут происходит МАГИЯ!!!

Мы вылезаем из только что отпаркованного лока и начинаем выполнять весь оставшийся в континуации код:

Таск пользователя завершается, и управление возвращается в таск файбера сразу же после инструкции continuation.run

При этом заканчивается и выполнение файбера, и мы снова оказываемся в режиме ожидания.

Следующий запуск файбера вновь инициирует весь описанный выше цикл перерождений.

Это про пару микробенчмарков, написанных за вечер? А кто вообще сказал, что все это работает?

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

У меня нет точного кода этого сервера, но если этот пост наберет достаточно лайков и комментариев, я попробую самостоятельно написать пример и построить реальные графики.

Да, конечно! Есть ли тут какие-то проблемы? Вся история с файберами — это история о сплошных проблемах и трейдоффах.

Философские проблемы

  • Нужно ли нам переизобрести треды?
  • Должен ли весь существующий код нормально работать внутри файбера?

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

Самых очевидных ограничений — 2 штуки. В чем заключаются технические ограничения?

Проблема раз — нельзя вытеснить нативные фреймы

PrivilegedAction<Void> pa = () -> { readLock.lock(); // may park/yield try { // } finally { readLock.unlock(); } return null;
}
AccessController.doPrivileged(pa); //native method

Здесь doPrivileged зовет нативный метод.

И в этот момент тред-носитель окажется запиненным до того времени, пока его не распаркуют. Вы в файбере зовете doPrivileged, выпрыгиваете из VMки, у вас на стеке появляется нативный фрейм, после чего вы пытаетесь запарковаться на строчке readLock.lock(). В этом случае могут закончиться треды-носители, и вообще, это ломает всю идею файберов. То есть тред пропадает.

Способ это решить уже известен, и сейчас идут дискуссии по этому поводу.

Проблема два — synchronized-блоки

Это уже гораздо более серьезная фигня

synchronized (object) { //may park object.wait(); //may park
}

synchronized (object) { //may park socket.getInputStream().read(); //may park
}

В случае захвата монитора в файбере, треды-носители тоже пинятся.

Это проблема. Понятно, что в совершенно новом коде можно поменять мониторы на прямые блокировки, вместо wait+notify можно использовать condition objects, но что делать с легаси?

В текущем прототипе для Thread и Fiber сделали один общий суперкласс под названием Strand.

Это позволяет перенести API в самом минимальном варианте.
Что делать дальше — как всегда в этом проекте, вопрос.

Что сейчас происходит с Thread API?

  • Первое использование Thread.currentThread() в файбере создает некий теневой тред, Shadow Thread;
  • с точки зрения системы, это "незапущенный" тред, и в нем нет никакой VMной метаинформации;
  • ST старается эмулировать все, что может;
  • но надо понимать, что в старом API куча мусора;
  • более конкретно, Shadow Thread реализует Thread API для всего, кроме stop, suspend, resume и обработки непойманных исключений.

Что делать с Thread Locals?

  • сейчас thread locals просто превращаются в fiber locals;
  • с этим есть очень много проблем, все это обсуждается;
  • особенно обсуждается набор способов использования;
  • треды исторически использовали и правильно, и неправильно (те, кто используют неправильно, все равно на что-то надеются, и нельзя их совсем-то разочаровывать);
  • в целом это создает целый спектр применений:
    • Высокоуровневые: кэш коннекшенов или паролей в контейнере;
    • Низкоуровневые: процессорные в системных библиотеках.

Thread:

  • Стек: 1MB и 16KB на структуры данных ядра;
  • На экземпляр треда: 2300 байтов, включая VMную метаинформацию.

Fiber:

  • Стек континуации: от сотен байт до килобайт;
  • На экземпляр файбера: 200-240 байтов.

Разница колоссальная!
И это именно то, что позволяет файберам запускаться миллионами.

Что сейчас поддерживается? Понятно, что самая магическая вещь — это автоматическая парковка при наступлении каких-то событий.

  • Thread.sleep, join;
  • java.util.concurrent и LockSupport.lock;
  • IO: сетевое на сокетах (socket read, write, connect, accept), файлы, пайпы;
  • Всё это недоделанное, но свет в туннеле виден.

Еще один вопрос, который все задают: как конкуррентно обмениваться информацией между файберами.

  • Текущий прототип запускает таски в Runnable, можно переделать на CompletableFuture, если зачем-то нужно;
  • java.util.concurrent «просто работает». Можно шарить всё стандартным способом;
  • возможно, появятся новые API для многопоточности, но это не точно;
  • куча мелких вопросов вроде «должны ли файберы возвращать значения?»; всё обсуждается, их нет в прототипе.

Иначе не получится держать их миллионами. На континуации накладываются очевидные требования: нужно использовать как можно меньше оперативной памяти, и нужно переключаться между ними как можно быстрей. И такая схема есть! Основная задача тут — каким-то образом не делать полное копирование стека на каждый паркинг-анпаркинг. Попробуем объяснить это в картинках.

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

Два джавовых массива в хипе. У нас есть два стула… в смысле, два стека. Второй — примитивный (например, интовый), который будет обрабатывать всё остальное. Один — объектный массив, где мы будем хранить ссылки на объекты.

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

run зовёт внутренний метод под названием enter:

И дальше выполняется пользовательский код, вплоть до первого вытеснения.

В этом прототипе это делается прямо физически — с помощью копирования. В этот момент выполняется вызов VM, который зовёт freeze.

Начинаем процесс последовательного копирования фреймов из нативного стека в джавовый хип.

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

И если всё хорошо, мы копируем сначала в примитивный массив:

Потом вычленяем ссылки на объекты и сохраняем в объектный массив:

Собственно, два чая всем, кто дочитал до этого места!

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

Мы всё перекопировали в заначку в хипе. Ура! Всё в хипе. Можно спокойно прыгать в место вызова, не боясь, что мы чего-то потеряли.

И она должна продолжиться с того места, где она была оставлена в прошлый раз. Теперь рано или поздно вызывающий код позовёт нашу континуацию снова. Это наша задача.

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

Плохая абстракция подобна котёнку с дверцей. Не уверен, что разморозка чая выглядит достаточно наглядно. Но нам и такая сгодится.

Производим вполне очевидные копирования.

Сначала с примитивного массива:

Потом со ссылочного:

Нужно немного попатчить скопированное, чтобы получить корректный стек:

Повторяем непотребство для всех фреймов:

Теперь можно вернуться к yield и продолжить, как будто ничего и не происходило.

Оно очень тормозное. Проблема в том, что полное копирование стека — это совершенно не то, что нам хотелось бы иметь. И главное — всё это линейно зависит от размера стека! Всё это вычленение ссылок, проверки для пиннинга, оно не быстрое. Не надо так делать. Словом, ад.

Вместо этого у нас есть другая идея — ленивое копирование.

Давайте откатимся к тому месту, где у нас уже есть замороженная континуация.

Мы продолжаем процесс так же, как и раньше:

Точно так же, как и раньше, чистим место на нативном стеке:

Но копируем не всё подряд, а только один или парочку фреймов:

Нужно пропатчить адрес возврата метода C, чтобы он указывал на некий return barrier: Теперь хак.

Теперь можно спокойно вернуться к yield:

Что в свою очередь приведёт к вызову пользовательского кода в методе C:

Но его вызывальщик — это B, и он не на стеке! Теперь представим, что C хочет вернуться к коду, который его вызвал. И, ну вы понимаете, это снова потянет за собой вызов thaw: Поэтому, когда он попытается вернуться, он пройдёт по адресу возврата, и этот адрес теперь — return barrier.

А thaw нам разморозит следующий фрейм на стеке континуации, и это B:

По сути, мы скопировали его лениво, по запросу.

И так раз за разом. Дальше мы сбрасываем B со стека континуации и снова устанавливаем барьер (барьер нужно ставить, потому что на стеке континуации кой-чего осталось).

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

В этом случае, когда придёт время делать freeze, нам нужно будет скопировать в стек континуации только верхушку нативного стека:

Оно линейно зависит только от количества тех фреймов, которые мы реально использовали в работе. Таким образом, количество совершаемой работы не зависит линейно от размера стека.

Некоторые фичи разработчики держат в уме, но в прототип они не попали.

  • Сериализация и клонирование. Возможность продолжить на другой машине, в другое время, и т.п.
  • JVM TI и отладка, как будто бы они обычные треды. Если вы заблокировались на чтении сокета, то вы не увидите красивого прыжка из yield, в прототипе тред просто заблокируется, как и любой другой обычный тред.
  • К хвостовой рекурсии даже не прикасались.

Следующие шаги:

  • Сделать человеческий API;
  • Добавить все недостающие фичи;
  • Улучшить производительность.

Cкачать прототип можно здесь, переключившись на бранч fibers. Прототип выполнен в виде бранча в репозитории OpenJDK.

Делается это так:

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

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

Я намекаю, что всячески не рекомендуется делать это на Windows. Во-вторых, нужно иметь нормально настроенный компьютер с C++ тулчейном и GNUшными либами. Это тот момент, когда msys заходит даже хуже, чем Cygwin. Серьезно, даже со скачиванием VirtualBox и установкой новой Ubuntu туда вы потратите на порядки меньше времени, чем пытаясь осознать очередную негуманоидную ошибку при билде из Cygwin или msys64.

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

Прочитать, что это такое, можно командой hg help -e fsmonitor.
Для включения нужно добавить в ~/.hgrc такую строчку: Если собираетесь чего-то менять в исходнике, рекомендую включить mercurial extension под названием fsmonitor.

[fsmonitor]
mode = on

Поэтому рекомендую на всякий случай сразу же после скачивания всю папочку скопировать куда-нибудь, cp -R ./loom ./loom-backup. Многие каким-то невообразимым образом умудряются испортить скачанный репозиторий в течение первых минут использования.

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

Например, если у вас свежеустановленная Ubuntu, то оно попросит установить Autoconf (sudo apt-get install autoconf). sh configure иногда будет просить что-то сделать. В Windows настолько же хороших подсказок не будет, если будут вообще. Это — одна из прелестей сборки OpenJDK на свежей Ubuntu, многие проблемы тебе уже рассказали, как решить.

Посмотреть, в чем разница между бранчами, можно командой hg diff --stat -r default:fibers.

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

Отсюда слово «микрофибра», например. Файбер в английском языке означает «волокно, нить». В Project Loom мы будем ткать свой код как полотно из файберов. «Loom» — это «ткацкий станок».

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

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

По-моему, аналогия прозрачна. А у нас дешевое производство тредов.

Всё это будет делать Искусственный Интеллект внутри IDE и так далее. Надеюсь, однажды всё это будет заменено на ещё более простые и дешевые по отношению к мыслетопливу разработчиков механизмы.

А в данный момент можно будет насладиться не только файберами, но и сочным бугуртом Свидетелей святой Конкарренси, которые расскажут нам, что всё это «не нужно», «тормозит», «вручную понятней» и так далее.

Напишите в комментариях. А что вы по этому поводу думаете? Порадуйте старого человека.

Спасибо.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть