skip to content

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?

level: principalimportance: nice to knowfreq 22%

answer

  1. Choose by access pattern, not platform
  2. Atomics: one value; Mutex: compound region; confine: many fields
  3. Mutex suspends, blocking lock doesn't
  4. Close newSingleThreadContext / terminate Worker
  5. StateFlow for observable state

basics

~20 s

Pick 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 s

Under 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 lines
kotlin
import 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

for a junior

Can name that locks or atomics protect shared state but not when to pick which.

for a middle

Maps atomics-to-one-value and Mutex-to-critical-section and knows Mutex suspends.

for a senior

Adds single-thread confinement, StateFlow, and the main failure modes (deadlock, blocking Main, leaks).

for a principal

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

context