skip to content

How does the new memory manager change multithreaded coroutines on Kotlin/Native, e.g. Dispatchers.Default and runBlocking with background work?

level: seniorimportance: should knowfreq 40%

answer

  1. Legacy: Dispatchers.Default was single-threaded on Native
  2. native-mt merged into mainline coroutines (1.6.0+)
  3. Continuations can now resume on other threads
  4. Real parallel Default/IO + newFixedThreadPoolContext
  5. Still need Mutex/atomics for shared state

basics

~20 s

Before, coroutines on Native couldn't freely jump threads because shared objects had to be frozen. The new manager lets coroutines move state across threads normally, so multithreaded dispatchers like Dispatchers.Default work like on the JVM.

solid answer

~40 s

Under the legacy model, kotlinx.coroutines on Native was essentially single-threaded per dispatcher: you couldn't capture mutable state and resume on a different thread without hitting freezing/InvalidMutabilityException, so multithreaded `Dispatchers.Default` was unavailable and patterns like `withContext(Dispatchers.Default)` for CPU work didn't behave like JVM. The new memory manager removes thread confinement, so kotlinx.coroutines ships a real multithreaded `Dispatchers.Default` and `Dispatchers.IO` (and `newFixedThreadPoolContext`), continuations can resume on different worker threads, and captured mutable state survives the hop. This requires the coroutines library built for the new MM (1.6.0+ with native-mt merged into the mainline). You still must synchronize shared mutable state accessed concurrently; structured concurrency and `Mutex` semantics are now consistent across JVM and Native, simplifying common KMP code.

code

kotlin · 17 lines
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val mutex = Mutex()
var shared = 0

suspend fun safeBump() = mutex.withLock { shared++ }

fun main() = runBlocking {
    coroutineScope {
        repeat(1000) {
            launch(Dispatchers.Default) { safeBump() } // parallel on workers
        }
    }
    println(shared) // 1000, thanks to Mutex (not the MM)
}

go deeper

for a junior

Knows coroutines now work across threads on Native like the JVM.

for a middle

Explains that legacy Dispatchers.Default was single-threaded and the new MM enabled real multithreaded dispatchers.

for a senior

Connects the MM to continuations resuming on other threads, the native-mt merge, and that synchronization is still required.

for a principal

Discusses KMP architecture impact: shared commonMain coroutine code, removal of dispatcher expect/actual workarounds, and consistent Flow/Mutex semantics across targets.

## The old constraint Kotlin/Native + kotlinx.coroutines under the legacy MM was effectively **thread-confined**. A coroutine suspended on one thread had to resume where its captured state was valid; moving mutable captured state to another thread tripped freezing or `InvalidMutabilityException`. The community used a special `native-mt` build to get limited multithreading, but it was fragile. Consequences: - `Dispatchers.Default` was **single-threaded** on Native. - `withContext(Dispatchers.Default) { cpuWork() }` did not give true parallelism. - Passing mutable objects into a coroutine that resumed elsewhere required freezing. ## What the new manager enables The new tracing GC removes thread ownership, so the runtime can let a coroutine **resume on any worker thread** while its captured mutable state remains valid. kotlinx.coroutines (from 1.6.0, with `native-mt` folded into the mainline) then provides: - A real **multithreaded `Dispatchers.Default`** (CPU pool) and **`Dispatchers.IO`**. - **`newFixedThreadPoolContext`** / `newSingleThreadContext` working as on the JVM. - Continuations that hop threads; `Mutex`, `Channel`, `Flow` behaving consistently with JVM. ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { val results = (1..4).map { i -> async(Dispatchers.Default) { // real parallel workers now heavyCompute(i) } }.awaitAll() println(results.sum()) } fun heavyCompute(i: Int): Int { /* CPU-bound */ return i * i } ``` ## What did NOT get automatic The MM removes **ownership** restrictions, not the need for **synchronization**: - Shared `var`/collections touched from multiple coroutines on multiple threads are still racy. Use `Mutex.withLock { }`, `kotlin.concurrent.AtomicInt/AtomicReference`, or confine state to a single coroutine. - Structured concurrency (`coroutineScope`, `supervisorScope`) is unchanged in shape but now genuinely parallel. ## KMP impact Because Native now matches JVM behavior, shared `commonMain` coroutine code (repositories, view-model logic) runs the same on both platforms, removing the old `expect/actual` dispatcher gymnastics and `native-mt`-specific dependencies.

  • Did the new memory manager make shared mutable coroutine state automatically race-free?
    No. It allows thread-hopping and shared mutation, but you must still synchronize with Mutex, atomics, or single-threaded confinement.
  • What was 'native-mt' and where did it go?
    A separate coroutines build that enabled limited Native multithreading; it was merged into mainline kotlinx.coroutines from 1.6.0 once the new MM landed.

saying these in an interview costs you the question

  • Claiming the MM made Dispatchers.Default thread-safe for shared state
  • Saying coroutines never worked on Native before the new MM
  • Forgetting that native-mt was folded into mainline coroutines
  • Thinking you still need a special coroutines artifact for multithreading
  • Asserting structured concurrency semantics changed fundamentally

context