You are designing a shared Kotlin Multiplatform module whose state is mutated from coroutines on Kotlin/Native. How do you choose between single-thread confinement, atomics, and a Mutex, and what failure modes do you guard against?
answer
- Choose by access pattern, not platform
- Atomics: one value; Mutex: compound region; confine: many fields
- Mutex suspends, blocking lock doesn't
- Close newSingleThreadContext / terminate Worker
- StateFlow for observable state
basics
~20 sPick by the shape of the state. Keep it on one thread if updates must stay ordered, use atomics for one simple value, and use a Mutex for a multi-step critical section. Watch for races, deadlocks, and blocking the main thread.
solid answer
~50 sUnder the new memory manager all three are valid because state is freely shared; the choice is about the access pattern, not the platform anymore. Use single-thread confinement (newSingleThreadContext or a dedicated Worker) when invariants span many fields and you want serialization without lock reasoning — at the cost of a dispatch hop and a hard concurrency cap of one. Use kotlin.concurrent atomics (AtomicInt/AtomicReference with CAS over immutable snapshots) for a single hot value where lock-free throughput matters. Use kotlinx.coroutines.sync.Mutex for compound critical sections that suspend; it is suspension-aware and won't block the thread, unlike a blocking lock. Failure modes to guard: data races from assuming old single-thread confinement; deadlocks/ordering issues with multiple Mutexes; blocking the iOS Main run loop with runBlocking or long work; ABA/lost-update in naive CAS; and thread/context leaks from unclosed newSingleThreadContext. Prefer StateFlow for observable state.
code
kotlin · 13 linesimport kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.concurrent.AtomicInt
val hits = AtomicInt(0) // hot scalar -> atomic
val mutex = Mutex() // compound region -> mutex
val ledger = mutableMapOf<String, Int>()
suspend fun record(user: String) {
hits.incrementAndFetch() // lock-free
mutex.withLock { ledger[user] = (ledger[user] ?: 0) + 1 } // serialized, suspend-safe
}go deeper
Can name that locks or atomics protect shared state but not when to pick which.
Maps atomics-to-one-value and Mutex-to-critical-section and knows Mutex suspends.
Adds single-thread confinement, StateFlow, and the main failure modes (deadlock, blocking Main, leaks).
Designs a coherent cross-target strategy, audits legacy single-threaded assumptions, and reasons about throughput vs correctness tradeoffs and lifecycle/leak management.
## Why this is now a design choice, not a constraint Before the new memory manager, freezing and single-thread coroutines forced the design. With the **NMM** and **multi-threaded coroutines**, state is shared freely and dispatchers are real pools — so you choose a strategy by **access pattern and invariants**. ## The three options ### 1. Single-thread confinement Confine all mutation to one thread: `newSingleThreadContext("state")` or a dedicated `Worker`. Every mutation runs serialized; you reason as if single-threaded. ```kotlin val stateCtx = newSingleThreadContext("state") // close it on shutdown! suspend fun mutate(block: () -> Unit) = withContext(stateCtx) { block() } ``` - **Pros:** simplest correctness for multi-field invariants; no lock-ordering puzzles. - **Cons:** a dispatch hop per access; concurrency capped at 1; the context must be `close()`d or it leaks a thread. ### 2. Atomics (lock-free) For one hot value, use `kotlin.concurrent.AtomicInt`/`AtomicReference` and a `compareAndSet` loop over **immutable** snapshots. - **Pros:** highest throughput, no blocking, no suspension. - **Cons:** only fits a single location; compound invariants don't fit; watch **lost updates / ABA** if you skip the retry loop or mutate the referent in place. ### 3. Mutex (suspending lock) `kotlinx.coroutines.sync.Mutex` (`withLock { }`) for a **compound critical section** that may suspend. ```kotlin val mutex = Mutex() suspend fun transfer() = mutex.withLock { /* multi-step, may suspend */ } ``` - **Pros:** suspension-aware — it parks the coroutine instead of blocking the thread; protects multi-step regions. - **Cons:** deadlock with multiple locks/bad ordering; serializes the section; not reentrant. ## Decision guide - One scalar/reference, hot path -> **atomics**. - Many fields, simple ordering desire -> **single-thread confinement**. - Multi-step region with suspension/IO inside -> **Mutex**. - Observable state others collect -> **`MutableStateFlow` + `update { }`** (atomic, lock-free, observable). ## Failure modes to guard against - **Stale single-thread assumptions:** code written for the old model assumes serialization; under the NMM it races. Audit shared `var`s. - **Blocking the Main run loop:** `runBlocking` or heavy work on `Dispatchers.Main` (iOS run loop) freezes the UI; offload to `Default`/`IO`. - **Deadlocks:** consistent global lock ordering; avoid nested `withLock`. - **Lost updates / ABA:** always loop on `compareAndSet`; keep referents immutable. - **Leaks:** `close()` every `newSingleThreadContext`/`newFixedThreadPoolContext`; terminate `Worker`s. - **Mixing primitives:** don't guard the same state with a Mutex in one path and raw atomics in another. ## Cross-target nuance The same module runs on JVM and Native; keep to multiplatform `kotlin.concurrent` atomics and `kotlinx.coroutines` so the strategy compiles everywhere, and remember `Dispatchers.Main` needs a real run loop on Apple targets.
- Why prefer a coroutine Mutex over a blocking platform lock inside suspend code?Mutex.withLock suspends the coroutine while waiting instead of blocking the underlying thread, so it doesn't starve the dispatcher's pool or freeze the Main run loop. A blocking lock holds the OS thread.
- When would single-thread confinement beat a Mutex even though both serialize access?When invariants span many fields and many call sites: confinement removes all lock-ordering reasoning and guarantees serialization globally, whereas scattered Mutex usage risks missing a path or deadlocking across multiple locks.
- What is the migration risk for a codebase written under the legacy single-threaded coroutines model?Code that implicitly relied on single-thread serialization now runs across pool threads, exposing latent data races and visibility bugs. You must audit shared mutable state and add atomics/Mutex/confinement.
Atomics are a single turnstile (one person at a time, fast); a Mutex is a meeting room you book for a whole task; single-thread confinement is hiring one clerk who does every change in order.
saying these in an interview costs you the question
- Claiming the platform still dictates the choice (freezing) rather than access pattern
- Using a blocking lock inside suspend code on the Main loop
- Guarding one value with both a Mutex and raw atomics inconsistently
- Ignoring close()/termination, leaking threads
- Assuming single-thread confinement is free of dispatch cost