Виктор Кинько

Корутины в Kotlin для лёгкого распараллеливания алгоритмов

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

Что это даёт Android-программисту? Как и любой синтаксический сахар - более читаемый код.
Виктор Кинько
Android-разработчик
Стоит заметить, что корутины это не только синтаксический сахар - из-за того, что они реализованы через легковесные потоки, их создание занимает куда меньше время, чем создание классических тредов, а значит, в системе с высоким параллелизмом они будут более выгодны в плане быстродействия. Но это лишь в теории.
На практике передо мной стояла тривиальная задача рефакторинга кода. Условия задачи:

- на экране есть Google-карта;
- на карте нужно отобразить маркеры, соответствующие точкам в пути;
- точки заданы текстовыми адресами.
Сложности и ход выполнения:

- Google-карту для начала нужно инициализировать. Это делается с помощью метода getMapAsync в отдельном потоке;
- для получения географических координат точек пути нужно использовать Geocoder, который выполняет интернет-запрос в отдельном потоке;
- после получения карты на неё можно добавлять маркеры, если они уже получены.
В итоге после вызволения логики в отдельный класс передо мной был такой код:
Перепишем этот код с использованием корутин:
Основное преимущество, которое мы получили, использовав корутины, - легкость модификации для распараллеливания.
Сейчас выполнение (и это явно видно в корутине) следует по следующей цепочке:

getPoints -> getGoogleMap -> addRouteOnMap
Но это неэффективно. Мы не инициализируем карту до того как не получены координаты точек. При слабой работе сети интернет мы можем ждать результат до самого таймаута.
С другой стороны, если поменять местами функции и вызвать сперва getGoogleMap, то на слабом телефоне придётся ждать пока инициализируется карта, а потом ещё и ждать результата запроса.
Лучший способ организации этого процесса - запустить две функции параллельно и передать результаты в третью:

getPoints  ->  addRouteOnMap  ->  getGoogleMap

Для организации этого способа в классических потоках, которые были представлены во втором примере, потребовалось бы ввести tread-safe флаги загрузки и по завершении каждой из функций getPoints и getGoogleMap проверять, завершилась ли параллельная функция. И только при успешном завершении обеих вызывать addRouteOnMap.
Для корутин же всё относительно просто. Нам требуется изменить только initMap:
Конечно же, это только способ организации кода и потоков, такой же как RX или просто цепочка вызовов. Но оцените, насколько просто удалось поменять ход выполнения, и насколько просто читается код. Если применить этот пример к более сложным алгоритмам, то можно добиться значительного прироста производительности, а значит, улучшить опыт пользователя.
Спасибо за внимание!