Explain the precise semantics of limitedParallelism: how parallelism is counted, what happens to excess coroutines, and the pitfalls of chaining or misusing it (e.g. on Dispatchers.Main or Unconfined).
answer
- Per-view FIFO gate of width n
- Each call => independent counter; reuse one view
- IO can exceed 64 for its limited slice
- Unconfined.limitedParallelism throws
- Don't chain; apply to a base dispatcher
basics
~20 sIt limits how many coroutines run at once on that view; extras queue and run as slots free up. Each call returns an independent view with its own limit. It's meant for multi-threaded dispatchers like IO/Default, not for Main or Unconfined.
solid answer
~50 slimitedParallelism(n) returns an independent dispatcher view whose own concurrency never exceeds n; the cap is enforced per-dispatch by counting active tasks, with excess coroutines parked in a FIFO queue and resumed as slots free. The view borrows threads from the parent — it does not subdivide the parent's limit, so `IO.limitedParallelism(100)` can exceed IO's default 64 because IO permits elastic expansion for that named slice (`Dispatchers.IO` is special-cased). Chaining (`view.limitedParallelism(m)`) is generally discouraged/unsupported and can throw; apply it once to a base dispatcher. It's intended for multi-threaded dispatchers; on `Dispatchers.Main` it has limited meaning (Main is already single-threaded) and on `Dispatchers.Unconfined` it throws `UnsupportedOperationException`. Calls dispatched to the view still suspend cooperatively, so an in-flight coroutine that suspends frees its slot only when it actually yields/finishes the dispatched block — long CPU-bound blocks hold the slot.
code
kotlin · 11 lines// WRONG: two views, combined parallelism = 4, not 2
val w1 = Dispatchers.IO.limitedParallelism(2)
val w2 = Dispatchers.IO.limitedParallelism(2)
// RIGHT: one shared view => true cap of 2
val shared = Dispatchers.IO.limitedParallelism(2)
async(shared) { /* ... */ }
async(shared) { /* ... */ }
// THROWS:
// Dispatchers.Unconfined.limitedParallelism(2)go deeper
Knows extras queue and the view caps concurrency; may not know edge cases.
Understands independent views and that you should reuse one view for a single cap.
Articulates per-dispatch slot accounting, IO's elastic special case, and the Unconfined/chaining pitfalls precisely.
Designs dispatcher topology for the whole app, anticipates version-specific UnsupportedOperationException behavior, and audits for accidental multi-view budget inflation.
## Counting parallelism `limitedParallelism(n)` tracks how many coroutines are **currently executing** on the view. When a coroutine is dispatched: - If fewer than `n` are running, it runs immediately (on a borrowed parent thread). - Otherwise it's **enqueued** (FIFO) and starts when a running one finishes its dispatched slice or **suspends back to the dispatcher**. A coroutine holds its slot for the duration of a **dispatched continuation**, not the whole coroutine lifetime — when it hits a suspension point that re-dispatches, the slot can be reused. So `n` bounds *simultaneously executing* continuations, not total in-flight coroutines. ## Independent views Each `limitedParallelism` call returns a **separate** view with its **own** counter: ```kotlin val a = Dispatchers.IO.limitedParallelism(2) val b = Dispatchers.IO.limitedParallelism(2) // a and b each allow 2 => up to 4 IO coroutines combined ``` They do **not** share a budget. To enforce one shared cap, create the view **once** and reuse it. ## Special case: Dispatchers.IO `Dispatchers.IO` is special-cased so that `IO.limitedParallelism(n)` can use up to `n` threads **even beyond IO's default soft limit of 64** — the slice is elastic. This is by design for blocking IO. On `Dispatchers.Default`, the view is still bounded by the CPU-sized pool. ## Pitfalls ### 1. Chaining ```kotlin val v = Dispatchers.IO.limitedParallelism(10) val w = v.limitedParallelism(5) // discouraged / may throw ``` Apply `limitedParallelism` to a **base** dispatcher, not to another limited view. Re-limiting a view is not a supported composition and can throw `UnsupportedOperationException` depending on version. ### 2. Unconfined / unsupported dispatchers `Dispatchers.Unconfined.limitedParallelism(n)` throws `UnsupportedOperationException` — Unconfined doesn't dispatch normally, so there's nothing to limit. ### 3. Dispatchers.Main Main is already effectively single-threaded; limiting it is usually meaningless and platform-dependent. ### 4. CPU-bound blocks hold slots A slot is freed only when the dispatched block finishes or suspends. A long, non-suspending CPU loop inside a `n=1` view will serialize everything behind it. ### 5. Creating views in hot paths Create the view once (e.g., a property), not per call — repeated creation defeats the shared-cap intent and adds overhead. ## Summary Think of it as a **per-view FIFO gate of width n** over borrowed threads: independent counters, elastic for IO, single-shot application, and not for Unconfined.
- Can Dispatchers.IO.limitedParallelism(100) use more than 64 threads?Yes. IO is special-cased so its limited slice can elastically use up to n threads even beyond IO's default 64 soft limit.
- What happens with Dispatchers.Unconfined.limitedParallelism(2)?It throws UnsupportedOperationException; Unconfined doesn't dispatch normally, so there's nothing to limit.
- If you call limitedParallelism(2) twice, is total concurrency 2?No. Each call is an independent view with its own counter, so combined concurrency is 4. Reuse one view for a single cap.
saying these in an interview costs you the question
- Assuming two views share one budget
- Chaining limitedParallelism on an already-limited view
- Calling it on Dispatchers.Unconfined and expecting it to work
- Believing IO can never exceed 64 even for a limited slice
- Creating a fresh view on every call in a hot path