Kotlin MultiplatformSeniorTechnical

Как KMP обрабатывает параллелизм на iOS (с учётом отсутствия разделяемого изменяемого состояния)?

KMP на iOS использует Dispatchers.Main через RunLoop главного потока и Dispatchers.Default через GCD. Для thread-safe состояния применяют Mutex и atomicfu; suspend-функции экспортируются в Swift через SKIE или ручные callback-обёртки.

Параллелизм KMP на iOS: от старой memory model к новой

До Kotlin 1.7.20 Kotlin/Native использовал strict memory model с концепцией ownership: объект принадлежал одному потоку, передача между потоками требовала freeze(). С Kotlin 1.7.20 введена новая memory model (по умолчанию с 1.9), совместимая с JVM-семантикой. Однако iOS runtime накладывает собственные ограничения: UI можно обновлять только с main thread, а NSObject не является thread-safe.

Dispatchers на iOS

Kotlin/Native реализует Dispatchers.Main через RunLoop главного потока iOS. Dispatchers.Default использует пул потоков через GCD (libdispatch). Без явного указания dispatcher coroutine выполняется в том потоке, откуда запущена.

// commonMain — suspend-функция в репозитории
class NewsRepository(
    private val api: NewsApi,
    private val cache: NewsCache
) {
    // suspend-функция безопасна для вызова с любого dispatcher
    suspend fun getNews(category: String): List<Article> =
        withContext(Dispatchers.Default) { // IO-работа на background thread
            val cached = cache.get(category)
            if (cached != null) return@withContext cached
            val remote = api.fetchNews(category)
            cache.put(category, remote)
            remote
        }
}

// commonMain — ViewModel с явными dispatcher-ами
class NewsViewModel(
    private val repo: NewsRepository,
    private val scope: CoroutineScope
) {
    private val _articles = MutableStateFlow<List<Article>>(emptyList())
    val articles: StateFlow<List<Article>> = _articles.asStateFlow()

    fun loadNews(category: String) {
        scope.launch(Dispatchers.Main) { // UI-обновление на main thread
            val news = repo.getNews(category) // переключается внутри
            _articles.value = news // StateFlow.value обновляется на Main
        }
    }
}

Экспорт coroutines в Swift

Swift не умеет вызывать suspend-функции напрямую. Два основных подхода: обёртка через callback или использование библиотеки KMP-NativeCoroutines / SKIE.

// Вариант 1: ручная обёртка через callback
// iosMain
fun NewsViewModel.loadNewsCallback(
    category: String,
    onSuccess: (List<Article>) -> Unit,
    onError: (Throwable) -> Unit
) {
    scope.launch {
        try {
            val news = newsRepo.getNews(category)
            withContext(Dispatchers.Main) { onSuccess(news) }
        } catch (e: Exception) {
            withContext(Dispatchers.Main) { onError(e) }
        }
    }
}
// Вариант 2: SKIE — автоматически генерирует Swift async/await
// build.gradle.kts
plugins {
    id("co.touchlab.skie") version "0.9.3"
}

// После этого Kotlin suspend-функции экспортируются как Swift async
// Swift-код:
// let news = try await viewModel.getNews(category: "tech")

Управление shared mutable state

Новая memory model позволяет обращаться к объектам из разных потоков, но race conditions по-прежнему возможны. Для thread-safe доступа используйте Mutex из kotlinx.coroutines или атомарные операции из kotlinx-atomicfu.

// commonMain — thread-safe кэш через Mutex
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class TokenStore {
    private val mutex = Mutex()
    private var token: String? = null

    suspend fun getToken(): String? = mutex.withLock { token }

    suspend fun setToken(value: String?) = mutex.withLock {
        token = value
    }
}

// AtomicFu для счётчиков без Mutex
import kotlinx.atomicfu.atomic

class RequestCounter {
    private val count = atomic(0)

    fun increment() = count.incrementAndGet()
    fun get() = count.value
}

Отмена coroutines и lifecycle iOS

В iOS нет ViewModel с автоматической отменой scope. Scope нужно создавать и отменять вручную, привязывая к lifecycle ViewController или SwiftUI View.

// commonMain — scope factory
object CoroutineScopeFactory {
    fun createMainScope(): CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main)
}
// Swift — управление lifecycle
class NewsViewController: UIViewController {
    private let scope = CoroutineScopeFactory.shared.createMainScope()
    private let viewModel: NewsViewModel

    init() {
        self.viewModel = NewsViewModel(
            repo: NewsRepository(),
            scope: scope
        )
        super.init(nibName: nil, bundle: nil)
    }

    deinit {
        // Отменяем все coroutines при уничтожении VC
        scope.cancel()
    }
}

Подводные камни

  • Вызов runBlocking с main thread iOS приводит к deadlock: iOS RunLoop и coroutine dispatcher конкурируют за main thread.
  • Без явного scope.cancel() в deinit coroutines продолжают работать после уничтожения ViewController — утечка памяти и ghost-обновления UI.
  • StateFlow собирается в Swift через .watch { } (из KMP-NativeCoroutines) — без отмены подписки Flow не завершается и держит ViewController в памяти.
  • Старые библиотеки, не обновлённые под новую memory model, могут бросать InvalidMutabilityException даже в Kotlin 1.9+.
  • Dispatchers.IO недоступен в Kotlin/Native — используйте Dispatchers.Default для фоновых операций на iOS.
  • Исключения из Kotlin coroutines не автоматически конвертируются в Swift Error — нужен @Throws аннотация или обёртка через Result.
  • Thread Sanitizer в Xcode может показывать ложные срабатывания для Kotlin/Native кода — проверяйте через Unit-тесты в commonTest.
  • Если CoroutineScope создан без SupervisorJob, падение одной дочерней coroutine отменяет весь scope — на iOS это убьёт все активные запросы.

Common mistakes

  • Объяснять «параллелизм на iOS» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Если не зафиксировать ownership состояния, баги проявятся как гонки, зависания UI или разные результаты на simulator/device.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «параллелизм на iOS» своими словами и связывает ее с кодом.
  • Называет механизм: Shared mutable state лучше ограничивать immutable моделями, state holders и explicit synchronization через coroutines primitives.
  • Видит production-последствие: Если не зафиксировать ownership состояния, баги проявятся как гонки, зависания UI или разные результаты на simulator/device.

Sources

Related topics