Pull to refresh

Comments 7

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

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

Как только речь заходит о реальных вычислениях, мы имеем целый ряд проблем:
1) Корутины реализованы, как сахар над JVM-тредами, которые, в конечном итоге, являются OS-тредами. Конечно, JVM-треды сидят в пуле, но anyway это треды со всеми плюсами и минусами. Важно понимать, что Корутины реализованы поверх JVM, а не внутри неё.

В противовес можно рассмотреть самые популярные корутины — Goroutines. Они реализованы в рантайме языка. Создать миллион Горутин — как два пальца об асфальт. А причина проста — горутина потребляет ВСЕГО несколько КБ памяти, а переключение контекста между Горутинами — стоит ничего. blog.nindalf.com/how-goroutines-work

2) В джаве, конечно, много проблем с тестами такого плана, как в статье. Столько приложений, и в каждом свой пул. Туда глянешь — пул томката из спринга. Сюда глянешь — вылезет дефолтный ForkJoinPool. И потом сидишь, чешешь репу: «а из-за кого это я просел по перфомансу»? «Может мой вебсёрвер кончился, аль я где-то все треды заблочил?».
Корутины тоже существуют только в рантайме языка и используют тред пул, если можно так выразиться, как среду исполнения. Насколько я понимаю, переключения контекстов минимальны — таски подбрасываются в очередь для CommonPool и потоки оттуда их забирают, не отдавая контроль операционной системе.

Сейчас для интереса запустил миллион корутин у себя на компе (простой async/await). Заняло 830мс, отъело 1,3 Мб, т.е. 132 бита на корутину. Замер пальцем в небо, без прогрева и т.д., но порядок такой.
Извиняюсь, если говорю ерунду, но я вижу Горутины vs Корутины так (буду благодарен, если поправите меня):

1) Котлиновские корутины реализованы в виде FSM — github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md#implementation-details

Условно говоря, любая корутина — это Континуейшен, который компилится в один класс (или вообще происходит оптимизация, и нет никакого класса, в том случае, если отложенный вызов происходит в самом конце корутины — tail calls оптимизация).

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

2) Горутины менеджерятся рантаймом Го. Они не позволяют пользователю явно указать, где нужно закончить/отложить вычисления. Вместо этого Горутины сами откладывают вычисления, если происходит IO блокировка, либо блокировка на примитивах синхронизации.

За счёт этого факта, а также за счёт того, что в Go реализована для горутин кооперативная многозадачность в userspace, то переключение контекста между Горутинами стоит практически ничего (так как мы точно знаем, какие регистры нам нужны в этот момент времени) — goo.gl/gGtWmB

3) Рассмотрим теперь Корутины vs Горутины. Пусть каждая из них засаспендилась, и нам требуется выбрать какую-то другую Ко(Го)рутину для исполнения. Тогда в Котлине нам придётся восстановить все возможные регистры, как в обычном, честном thread context switch. В то время, как в Го — мы поменяем лишь пару регистров (спасибо рантайму Го за это).

Кажется, что в этом месте мы имеет большую перфоманс разницу. Или я где-то ошибся в рассуждениях?
Рассмотрим теперь Корутины vs Горутины. Пусть каждая из них засаспендилась, и нам требуется выбрать какую-то другую Ко(Го)рутину для исполнения. Тогда в Котлине нам придётся восстановить все возможные регистры, как в обычном, честном thread context switch. В то время, как в Го — мы поменяем лишь пару регистров (спасибо рантайму Го за это).

Все строго наоборот. В "корутинах" (формально, stackless coroutines) каждый фрагмент выполнения является отдельным вызовом функции — а потому нет никакой необходимости сохранять регистры между вызовами. Все требуемое состояние уже сохранено в полях сгенерированного объекта и лежит в куче.


В "горутинах" (формально, stackfull coroutines) происходит самое настоящее переключение контекста, пусть и без захода в ядро. Поэтому тут приходится по-честному сохранять и восстанавливать регистры.


Возможно, без примера несколько непонятно. Допустим, у нас есть вот такой код:


var a = foo();
bar();
baz(a);

В случае stackless coroutines [JIT-]компилятор знает в какой момент может произойти приостановка и может ли она произойти в принципе. Исходя из этого можно принять решение о том где хранить переменную a — в стеке, в куче или в регистре.


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

много проблем с тестами такого плана

Специально писал такие тесты, чтобы было поближе к жизни. Вряд ли кто-то ради корутин слезет со спринга. А вот на Jetty пересесть можно. Примерно глянул в ту сторону, к Jetty можно свой тред пул подбросить. Но надо разбираться, что от этого пула ждет сам Jetty, так что углубляться пока не стал.

Начиная с версии 5 Spring вполне умеет быть реактивным.


Более того, корутины без проблем живут в Spring — т.к. он использует Project Reactor для асинхронности, а у kotlinx.coroutines есть поддержка Reactor.


Вообще, странно противопоставлять "спринг", Jetty и/или корутины, это как бы ортогональные вещи.

Sign up to leave a comment.

Articles

Change theme settings