Каковы ограничения Kotlin/Native в отношении coroutines и многопоточности?
Старый MM требовал заморозки объектов для передачи между потоками (freeze()), новый MM с Kotlin 1.7.20 снял это ограничение. Главные актуальные проблемы: suspend функции не экспортируются как Swift async/await (нужен SKIE), нет lifecycle-aware scope для iOS.
Эволюция модели памяти в Kotlin/Native
История coroutines и многопоточности в Kotlin/Native делится на два периода: до и после нового Memory Manager (New MM), включённого по умолчанию в Kotlin 1.7.20.
Старый Memory Manager (до 1.7.20): «freeze или ошибка»
В оригинальной модели памяти Kotlin/Native действовало строгое правило: объект может быть изменён только из одного потока. Попытка передать мутабельный объект в другой поток вызывала InvalidMutabilityException. Для передачи данных между потоками объект нужно было «заморозить» через freeze(), после чего он становился immutable навсегда.
Это делало coroutines крайне неудобными: Dispatchers.Default на Native использовал пул потоков, и любой мутабельный state в coroutine мог упасть с непонятной ошибкой.
Новый Memory Manager (Kotlin 1.7.20+)
Новый ММ принёс модель памяти, аналогичную JVM: объекты могут быть разделены между потоками без заморозки. Concurrent GC работает параллельно с приложением, что снижает паузы.
// shared/src/commonMain/kotlin/com/example/CoroutineExample.kt
import kotlinx.coroutines.*
class DataRepository {
private val _data = mutableListOf<String>()
// С новым MM это работает на Native без ошибок
suspend fun loadData(): List<String> = withContext(Dispatchers.Default) {
// Симуляция сетевого запроса
delay(100)
_data.add("item") // Мутация в background thread — OK в новом MM
_data.toList()
}
fun processInBackground(block: suspend () -> Unit): Job {
return CoroutineScope(Dispatchers.Default).launch {
block()
}
}
}
Текущие ограничения coroutines на Kotlin/Native
1. Dispatchers.Main
На iOS Dispatchers.Main требует зависимости kotlinx-coroutines-core с Native таргетом. Без неё Dispatchers.Main не существует. Начиная с kotlinx.coroutines 1.7.0, Main dispatcher на iOS реализован через CFRunLoop.
// build.gradle.kts
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
// Отдельная зависимость для iosMain больше не нужна — core включает Native реализацию
2. Интеграция со Swift async/await
Suspend функции не экспортируются нативно как Swift async функции. Из Swift они выглядят как функции с completionHandler:
// Kotlin
class UserService {
suspend fun fetchUser(id: String): User {
return api.getUser(id)
}
}
// В Swift без дополнительных библиотек:
// userService.fetchUser(id: "123") { user, error in ... }
// Не идиоматично для Swift 5.5+
Для нормальной интеграции используют одно из двух решений:
- SKIE (Swift Kotlin Interface Enhancer) от Touchlab: автоматически генерирует Swift async/await обёртки для suspend функций, sealed classes → Swift enums, Flow → AsyncSequence.
- KMP-NativeCoroutines от rickclephas: аннотации + Gradle плагин, генерирует Swift-friendly API.
// С SKIE — достаточно обычного Kotlin кода
// SKIE автоматически генерирует:
// func fetchUser(id: String) async throws -> User
// build.gradle.kts
plugins {
id("co.touchlab.skie") version "0.9.0"
}
3. Flow и Swift
kotlinx.coroutines Flow не имеет прямого Swift-эквивалента. SKIE конвертирует Flow в AsyncSequence:
// Kotlin
class WeatherRepository {
fun temperatureUpdates(): Flow<Double> = flow {
while (true) {
emit(fetchTemperature())
delay(5000)
}
}
}
// Swift с SKIE:
// for await temp in weatherRepository.temperatureUpdates() {
// updateUI(temp)
// }
4. CoroutineScope и lifecycle на iOS
На Android есть viewModelScope и lifecycleScope. На iOS lifecycle управляется иначе — нет встроенной интеграции с UIViewController/SwiftUI View lifecycle. Нужно вручную отменять scope при deinit:
// Kotlin: base class для iOS ViewModels
open class BaseViewModel {
protected val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Main
)
fun onCleared() {
scope.cancel()
}
}
// Swift:
class MyViewController: UIViewController {
let viewModel = MyViewModel()
deinit {
viewModel.onCleared() // критично не забыть
}
}
Подводные камни
- runBlocking недоступен на Main thread iOS: вызов runBlocking на главном потоке iOS приводит к deadlock, потому что Main dispatcher использует тот же runloop. Используйте только в тестах или фоновых потоках.
- Утечки памяти через retain cycles: circular references между Kotlin объектами и Swift closure не всегда обнаруживаются GC. Особенно опасны при передаче callbacks из Swift в Kotlin.
- Старые статьи с .native-mt артефактом: до kotlinx.coroutines 1.6.0 для Native нужен был отдельный артефакт kotlinx-coroutines-core-native-mt. С 1.6.0 основной артефакт поддерживает Native с новым MM.
- StateFlow и SwiftUI: StateFlow не интегрируется со SwiftUI @ObservableObject напрямую. Нужны обёртки (SKIE делает это автоматически через ObservableObject extension).
- Dispatchers.IO на Native: Dispatchers.IO на iOS эмулируется через Dispatchers.Default (фиксированный пул потоков), а не через безлимитный пул как на JVM. Это важно для приложений с большим количеством параллельных I/O операций.
- Exception propagation: непойманные исключения в coroutine на Native крашат приложение через
terminate(), а не через UncaughtExceptionHandler как на JVM/Android. Всегда используйте CoroutineExceptionHandler в production-корутинах. - Structured concurrency и Objective-C callbacks: при интеграции с ObjC API (UIKit completion handlers) легко нарушить structured concurrency — callback приходит вне Kotlin coroutine context. Используйте suspendCancellableCoroutine для правильной интеграции.
- SKIE vs KMP-NativeCoroutines: оба решения работают, но несовместимы друг с другом. Выбирайте одно на старте проекта — миграция болезненна.
Common mistakes
- Объяснять «coroutines и многопоточность в Kotlin/Native» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Считать Native coroutines JVM-копией опасно: проблемы проявятся как гонки, frozen assumptions или ограничения interop.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «coroutines и многопоточность в Kotlin/Native» своими словами и связывает ее с кодом.
- Называет механизм: На Native важно проверять актуальную memory model, main-thread UI constraints и доступность dispatcher-а для платформы.
- Видит production-последствие: Считать Native coroutines JVM-копией опасно: проблемы проявятся как гонки, frozen assumptions или ограничения interop.