Использование корутин в Kotlin для лёгкого распараллеливания алгоритмов

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

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

На практике передо мной стояла тривиальная задача рефакторинга кода. Условия задачи:

• на экране есть Google-карта

• на карте нужно отобразить маркеры, соответствующие точкам в пути

• точки заданы текстовыми адресами.

Сложности и ход выполнения:

• Google-карту для начала нужно инициализировать. Это делается с помощью метода getMapAsync в отдельном потоке

• Для получения географических координат точек пути нужно использовать Geocoder, который выполняет интернет-запрос в отдельном потоке

• После получения карты на неё можно добавлять маркеры, если они уже получены. Это должно делаться в главном потоке.

В итоге после вызволения логики в отдельный класс передо мной был такой код:

//Использование во fragment MapUtils.initGoogleMap(map) { googleMap -> MapUtils.setupMap(context, googleMap, handlePointsCallback) } class MapUtils { companion object { //Получаем карту. Callback вызывается в UI потоке fun initGoogleMap(map: CustomMapFragment, callback: (googleMap: GoogleMap) -> Unit) { map.getMapAsync { googleMap -> callback(googleMap) } } //Добавить метки на карту fun setupMap( ctx: Context, googleMap: GoogleMap, addresses: ArrayList<String>, callback: (points: ArrayList<LatLng>) -> Unit ) { //Вызываем Geocoder в отдельном потоке doAsync { val points = getLatLngFromGeocoder(ctx, addresses) //Только после получения координат добавляем их на карту, обязательно в UI потоке uiThread { addRouteOnMap(googleMap, points, addresses) callback(points) } } } } }

Перепишем этот код с использованием корутин:

//Использование во fragment MapUtils.initMap(map, addressesNames, handlePointsCallback) class MapUtils { companion object { //Функция инициализация карты и отрисовки на ней точек @ExperimentalCoroutinesApi fun initMap( map: CustomMapFragment, addresses: ArrayList<String>, callback: (points: ArrayList<LatLng>) -> Unit ) { //Запускаем корутины в параллельном потоке CoroutineScope(Dispatchers.Default).launch { //Получаем точки от geocoder val points = getPoints(map, addresses) //Инициализируем карту val googleMap = getGoogleMap(map) //Если не получили координаты, то и отрисовывать нечего points ?: return@launch //В главном потоке withContext(Dispatchers.Main) { //возвращаем точки во фрагмент callback(points) //наносим на карту addRouteOnMap(googleMap, points, addresses) } } } //Приостанавливаемая функция получения точек от geocoder private suspend fun getPoints(map: CustomMapFragment, addresses: ArrayList<String>): ArrayList<LatLng>? = withContext(Dispatchers.Default) { map.context?.let { context -> getLatLngFromGeocoder(context, addresses) } } //вспомогательная функция для превращения callback в suspend fun @ExperimentalCoroutinesApi suspend fun awaitCallback(block: (OnMapReadyCallback) -> Unit): GoogleMap = suspendCoroutine { cont -> block(OnMapReadyCallback { googleMap -> cont.resume(googleMap) }) } //Приостанавливаемая функция получения google-карты, с переводом callback в suspendable через awaitCallback @ExperimentalCoroutinesApi private suspend fun getGoogleMap(map: CustomMapFragment): GoogleMap = withContext(Dispatchers.Main) { return@withContext awaitCallback { block -> map.getMapAsync(block) } } } }

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

Сейчас выполнение (и это явно видно в корутине) следует по следующей цепочке: getPoints -> getGoogleMap -> addRouteOnMap

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

С другой стороны, если поменять местами функции и вызвать сперва getGoogleMap, то на слабом телефоне придётся ждать пока инициализируется карта, а потом ещё и ждать результата запроса.

Лучший способ организации этого процесса - запустить две функции параллельно и передать результаты в третью:

@ExperimentalCoroutinesApi fun initMap( map: CustomMapFragment, addresses: ArrayList<String>, callback: (points: ArrayList<LatLng>) -> Unit ) { CoroutineScope(Dispatchers.Default).launch { //функции, вызванные через GlobalScope.async будут исполняться параллельно val googleMap = GlobalScope.async { getGoogleMap(map) } val points = GlobalScope.async {getPoints(map, addresses)} withContext(Dispatchers.Main) { //если к этому моменту функция getPoints не завершит выполнение корутина приостановится points.await()?.let { points -> callback(points) //аналогично с getGoogleMap addRouteOnMap(googleMap.await(), points, addresses) } } } }

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

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

Спасибо за внимание!

Больше статей в нашем блоге:

44
Начать дискуссию