How does capturing a mutable var interact with inline functions and concurrency? Discuss inline vs non-inline capture, and the thread-safety of mutating a captured var from multiple threads/coroutines.
answer
- Inline -> body pasted, captured var stays local, no Ref
- noinline -> real object; crossinline -> inlined, no non-local return
- Non-inline -> shares Ref box for captured var
- Ref.element not volatile/atomic -> data race
- Use AtomicInteger/atomicfu/Mutex/StateFlow instead
basics
~20 sInline lambdas paste their body in place, so a captured var often needs no wrapper. But two threads mutating the same captured var race — the Ref field isn't synchronized; use atomics or proper concurrency instead.
solid answer
~40 sAn inline function inlines the lambda body at the call site, so a captured var can stay an ordinary local — usually no Ref allocation and no escape. A non-inline lambda (stored, returned, crossinline, passed to a non-inline fn, or surviving across a coroutine suspension) becomes a real object that shares a Ref.IntRef/ObjectRef with the enclosing scope. Concurrency is the danger: mutating a captured var from multiple threads or concurrently running coroutines is a data race — Ref.element is a plain non-volatile, non-atomic field, so increments lose updates and visibility isn't guaranteed. Don't accumulate into a captured var across launch{} coroutines. Use AtomicInteger/atomicfu, a Mutex, confine to one thread, or model state with StateFlow/channels. The capture mechanism gives shared mutable state with none of the safety.
code
kotlin · 11 linesimport kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicInteger
suspend fun safeCount(): Int = coroutineScope {
val counter = AtomicInteger(0) // capture the atomic, not a plain var
repeat(1000) {
launch(Dispatchers.Default) { counter.incrementAndGet() }
}
// children join at end of coroutineScope
counter.get() // reliably 1000
}go deeper
May know inline 'pastes' code but not the capture/concurrency consequences.
Distinguishes inline vs non-inline capture and knows shared var mutation across threads is unsafe.
Explains noinline/crossinline semantics, the Ref non-atomic field, and names AtomicInteger/Mutex/StateFlow fixes.
Designs concurrency around immutable accumulation/structured concurrency, sets conventions banning shared captured-var mutation, and reasons about happens-before/visibility.
## Inline vs non-inline capture Kotlin's `inline` functions copy the lambda **body** into the call site instead of creating a function object: - **Inline** (`let`, `run`, `forEach`, `repeat`, custom `inline fun`): the lambda doesn't escape; a captured `var` can remain a normal JVM local. Typically **no `Ref` box** and no allocation. - **`noinline`** parameter: forces that specific lambda to be a real object (so it can be stored/passed) — captures behave like the non-inline case (Ref box for vars). - **`crossinline`** parameter: still inlined, but forbidden from non-local returns because it may be invoked from another execution context; captures are inlined but you must treat the lambda as possibly running elsewhere. - **Non-inline** lambda (stored in a `val`, returned, passed to a non-inline higher-order function): compiled to a class that shares a `kotlin.jvm.internal.Ref` holder with the enclosing scope for any captured `var`. ```kotlin inline fun timed(block: () -> Unit) { val t = System.nanoTime(); block(); /* ... */ } // `block` is inlined; a var it captures usually stays a local, no Ref. ``` ## The concurrency hazard Capturing a `var` gives you **shared mutable state** with *no* built-in synchronization. The `Ref.element` field is a plain field — not `volatile`, not atomic. So mutating a captured var from multiple threads or concurrently scheduled coroutines is a **data race**: ```kotlin // BUG: races on the captured var var counter = 0 coroutineScope { repeat(1000) { launch(Dispatchers.Default) { counter++ } } } println(counter) // often < 1000 — lost updates, no visibility guarantee ``` `counter++` is read-modify-write; concurrent increments interleave and lose updates, and there's no happens-before edge guaranteeing one thread sees another's write. ## Correct alternatives - **`java.util.concurrent.atomic.AtomicInteger`** (or kotlinx-atomicfu `atomic(0)`): capture the atomic and call `incrementAndGet()`. - **`Mutex`** (kotlinx.coroutines) around the mutation, or confine all mutations to a single thread/dispatcher. - **Immutable accumulation**: have each coroutine return a value and combine results (`map { async { ... } }.awaitAll().sum()`), avoiding shared state. - **`StateFlow`/`MutableStateFlow`** for observable, atomically-updated state (`update { it + 1 }`), or actors/channels to serialize mutations. ## Design framing Captured-var mutation is convenient for single-threaded accumulation (a running total in a sequential loop) but is the wrong tool for concurrent code. The capture mechanism shares the variable; it does **not** make it thread-safe. Choose explicit concurrency primitives when more than one thread or coroutine writes.
- Why does an inline forEach capturing a var usually allocate no Ref?The lambda body is inlined into the loop, so the captured var stays an ordinary local variable; there's no separate lambda object that needs a shared holder.
- What does crossinline change about a captured lambda compared to a plain inline parameter?crossinline keeps the lambda inlined but disallows non-local returns from it, because it may be invoked from a different execution context (e.g. inside another lambda or runnable).
- Is incrementing a captured Int var from two coroutines on Dispatchers.Default safe?No. It's a data race on a non-atomic field; use AtomicInteger, a Mutex, single-thread confinement, or StateFlow.update instead.
Capturing a var across threads is like several people writing on one sticky note at once: convenient for one person, chaos for many — you need a lock or separate notes.
saying these in an interview costs you the question
- Claiming the Ref box makes captured var mutation thread-safe
- Saying inline and non-inline lambdas allocate identical capture wrappers
- Recommending a plain captured var for concurrent accumulation
- Confusing crossinline (no non-local return) with noinline (becomes an object)
- Believing counter++ on a captured var is atomic