Корутины в KMP и нюансы iOS

Корутины — общий язык асинхронности в KMP; разбираем диспетчеры и подводные камни iOS.

Корутина — приостанавливаемая единица асинхронной работы; в KMP корутины работают на всех платформах и являются стандартным способом писать неблокирующий общий код.

Зачем корутины общему коду

Сетевой запрос, чтение из базы, любая долгая операция в общем коде — это suspend-функция. Корутины дают единый, платформенно-нейтральный способ их выполнять и комбинировать. Библиотека kotlinx.coroutines мультиплатформенна и идёт в commonMain.

Dispatchers по платформам

Диспетчер решает, на каком потоке выполняется корутина. Dispatchers.Default (пул для CPU-задач) и Dispatchers.Main (UI-поток) есть на обеих платформах. А вот Dispatchers.IO исторически был только на JVM; в общем коде для блокирующего ввода-вывода полагаются на Default или платформенную реализацию. На iOS Dispatchers.Main — это главная очередь, корректная для обновления SwiftUI.

// commonMain
class OrderRepository(private val api: OrderApi) {
    suspend fun refresh(): List<OrderDto> =
        withContext(Dispatchers.Default) { api.fetchOrders() }
}

Структурированная конкурентность

Корутины запускаются в CoroutineScope, который владеет их жизненным циклом: отменили scope — отменились все его корутины. В общем ViewModel это критично: при уходе с экрана отменяем scope, и зависшие запросы не утекают.

Как работает под капотом на iOS

Раньше Kotlin/Native имел жёсткую модель памяти: объект, переданный в другой поток, нужно было «заморозить», иначе падение. Это сильно усложняло многопоточные корутины на iOS. С новым менеджером памяти Kotlin/Native (memory manager) это ограничение снято — объекты свободно делятся между потоками, как на JVM. Современный KMP-проект пишет корутины почти одинаково для обеих платформ. Тем не менее на iOS остаётся нюанс: длинные CPU-задачи на главной очереди подвесят UI так же, как на любой платформе, поэтому тяжёлое выносят на Default.

Вызов из Swift

suspend-функции автоматически экспортируются в Swift как функции с completion-обработчиком (а в новых версиях — как async). Подробнее — в разделе про интероп. Главное помнить: из общего кода вы отдаёте Swift корутинный API, а не блокирующий.

Частые ошибки

Полагаться на Dispatchers.IO в общем коде — его там может не быть; используйте Default или абстрагируйте диспетчер. Вторая ошибка — запускать корутины в «глобальном» scope без привязки к экрану: они переживут экран и утекут. Третья — держать в голове старую модель «замораживания»; на современном KMP она неактуальна, не усложняйте код её обходами.

Итоги

  • Корутины — общий способ асинхронности в KMP, библиотека мультиплатформенна.
  • Default/Main есть везде; IO — нюанс, в общем коде на него не полагаются.
  • Структурированная конкурентность через scope предотвращает утечки.
  • Новый менеджер памяти Kotlin/Native снял ограничения «замораживания» на iOS.
Проверьте себя
1. Какой диспетчер исторически отсутствовал в общем коде KMP?
ADispatchers.Default
BDispatchers.Main
CDispatchers.IO
DDispatchers.Unconfined
2. Что изменил новый менеджер памяти Kotlin/Native?
AУдалил корутины с iOS
BСнял требование 'замораживать' объекты при передаче между потоками
CЗапретил многопоточность
DСделал iOS медленнее