skip to content

Concurrency on Native

Native concurrency now uses ordinary worker threads, atomics, and multi-threaded coroutine dispatchers under the new memory manager. The main-thread-only restriction that used to define this topic is gone, which is itself the point.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

On Kotlin/Native with the new memory manager, can you share a mutable object between threads, and what changed compared to the old model?

level: juniorimportance: must knowfreq 55%

answer

  1. New manager = no freezing
  2. Default since 1.7.20
  3. freeze()/isFrozen now no-ops
  4. Sharing != thread-safe
  5. Use kotlin.concurrent atomics for mutation

basics

~10 s

Yes. With the new memory manager you can pass and use the same object from several threads. The old model was much stricter and required freezing objects first; that is gone now.

solid answer

~40 s

Under the new memory manager (default since Kotlin 1.7.20, the only model now), objects are shared freely across threads without freezing. The old legacy manager required objects to be 'frozen' (deeply immutable) before crossing a thread boundary, throwing InvalidMutabilityException otherwise, and split the heap into thread-local and shared/frozen regions. The new manager removes that: any object can be referenced from multiple threads, mutable state included. However, sharing does not give you thread-safety for free — concurrent mutation still needs synchronization (kotlin.concurrent.AtomicReference/AtomicInt, locks, or confining mutation to one thread). freeze() and ensureNeverFrozen() are now deprecated no-ops. The key shift: the language no longer enforces immutability at the boundary; correctness of shared mutation is your responsibility.

code

kotlin · 12 lines
kotlin
import kotlin.native.concurrent.Worker
import kotlin.native.concurrent.TransferMode

class Box(var value: Int = 0)

fun main() {
    val box = Box()              // no freeze() required under the new MM
    val worker = Worker.start()
    val f = worker.execute(TransferMode.SAFE, { box }) { it.value + 41 }
    println(f.result)            // 41
    worker.requestTermination().result
}

go deeper

for a junior

Knows freezing is gone and objects can be shared without it.

for a middle

Adds that sharing still needs synchronization and names atomics/locks.

for a senior

Explains the deprecation of freeze/isFrozen, TransferMode being legacy, and the 1.7.20 default.

for a principal

Frames the change as aligning Native with JVM semantics and enabling multi-threaded coroutines; reasons about migration risk in old codebases relying on freeze.

## The old world: freezing Kotlin/Native originally shipped a strict memory model. The heap was divided into per-thread (mutable, thread-local) objects and a shared region. To move an object to another thread or store it where another thread could see it, you had to **freeze** it with `obj.freeze()`. Freezing made the whole object graph **deeply immutable** at runtime. Touching a frozen object's mutable field threw `InvalidMutabilityException`. Sharing a non-frozen object threw `IncorrectDereferenceException`. This made cross-thread code painful and surprising. ## The new memory manager The **new memory manager** (NMM) became default in **Kotlin 1.7.20** and is the only supported model today. Its headline change: **objects are shared across threads with no freezing**. The thread-local/frozen split is gone. You can create an object on one thread, hand it to another `Worker`, and mutate it — the runtime no longer rejects this. ```kotlin import kotlin.native.concurrent.Worker class Counter(var n: Int = 0) fun main() { val shared = Counter() // no freeze needed val w = Worker.start() val future = w.execute( TransferMode.SAFE, // still required by the API { shared } ) { c -> c.n + 1 } println(future.result) w.requestTermination().result } ``` ## Sharing is not safety A crucial nuance: the NMM lets you *share* mutable state, but it does **not** make concurrent mutation safe. Two threads incrementing `var n` race. You still need: - `kotlin.concurrent.AtomicInt`, `AtomicLong`, `AtomicReference` for lock-free atomics. - Locking primitives or single-threaded confinement. - Coroutine structures (`Mutex`, channels, `StateFlow`). ## Deprecated APIs `freeze()`, `isFrozen`, and `ensureNeverFrozen()` from `kotlin.native.concurrent` are **deprecated** and behave as no-ops under the NMM. New code must not depend on them. ## Why it matters The NMM aligns Kotlin/Native's concurrency story with the JVM: shared mutable state is *possible* but must be *synchronized*. It also unlocked **multi-threaded coroutines**, removing the old single-thread restriction on the main dispatcher.

  • If freezing is gone, why does the Worker.execute API still take a TransferMode?
    It is kept for source/binary compatibility. Under the NMM TransferMode.SAFE no longer enforces detachment; objects are simply shared. The parameter is effectively legacy.
  • What replaces freeze() for guaranteeing immutability?
    Nothing at runtime — use the type system: val properties and immutable data classes. The runtime no longer enforces deep immutability at thread boundaries.

The old model locked the door (freeze) so no two people could touch a room's furniture; the new model unlocks every door but you still must agree on who moves the couch.

saying these in an interview costs you the question

  • Claiming you still must call freeze() before sharing
  • Saying shared objects are automatically thread-safe
  • Confusing the new manager with the JVM's GC details
  • Thinking InvalidMutabilityException is still a normal occurrence
  • Believing the heap is still split into thread-local vs frozen regions

context

open as a page

How do you do lock-free shared mutable state on Kotlin/Native today, and which atomic types do you use?

level: middleimportance: must knowfreq 48%

basics

~10 s

Use the atomic types from kotlin.concurrent, such as AtomicInt and AtomicReference. They let several threads update one value safely without locks, using operations like compareAndSet and incrementAndGet.

open as a page

Explain the Worker API on Kotlin/Native: how do you start one, run work on it, and get results back?

level: middleimportance: should knowfreq 38%

basics

~10 s

A Worker is a background thread. You start it with Worker.start(), send work with execute(), which returns a Future. You read the answer from future.result, and stop the worker with requestTermination().

open as a page

What did multi-threaded coroutines support change on Kotlin/Native, and how do dispatchers behave under the new memory manager?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Coroutines on Native used to run on one thread only. Now they can run across many threads. Dispatchers.Default uses a real thread pool, and a coroutine can resume on a different thread than it started on.

open as a page

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%

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.

open as a page