Domains: True Parallelism
Everything so far — even our elegant coroutines — has been one core taking turns with itself. Today OCaml 5 hands you the actual cores of your actual CPU. The browser can't follow us here, so fire up the terminal: this whole lesson is a localhost lab.
You will be able to…
- Explain what a domain is — and why the in-page engine can't have them
- Fan work out with
Domain.spawnand collect it withDomain.join - Measure a real speedup with the shell's
timecommand - Demonstrate a data race losing updates, then fix the counter with
Atomic
Why the browser sits this one out
Camel Quest has run in this page since lesson 1.1 because the engine is real OCaml compiled to JavaScript by js_of_ocaml. That inheritance comes with JavaScript's oldest rule: a page gets one thread. Lesson 4.2's coroutines danced beautifully inside that rule — pausing, resuming, taking turns — but it was always one thing happening at a time. Meanwhile your machine has been sitting on four, eight, maybe sixteen cores, lending OCaml exactly one of them.
Native OCaml 5 has no such rule. Its unit of parallelism is the domain: spawn one and your program is genuinely running in two places at once, on two cores, sharing one memory. This is the "multicore" that made OCaml 5.0 famous, and it is native-only — so today the site checks nothing and your own eyes check everything. You'll need the toolchain from lesson 1.3.
Spawn and join
The API you need today is two functions. Open a local toplevel
(opam exec -- ocaml) and try them:
Domain.spawn f starts f () running
immediately, on another core, and hands you a ticket of type
'a Domain.t. Domain.join trades the ticket for
the result, waiting if the work isn't finished. Spawn is placing an order;
join is picking it up. And recommended_domain_count tells you
how many domains this machine can keep genuinely busy — mine says 8;
yours may differ, and that number matters later.
The lab, part 1: feel the cores
Talk is cheap; let's melt some silicon. Two programs, same four slabs of work — an intentionally slow Fibonacci stands in for "generate a galaxy sector". The first runs the slabs one after another; the second gives each slab its own domain. Save both files side by side:
Notice what bench_domains.ml does not contain: no
locks, no messages, no cleverness. Four independent jobs,
Array.map Domain.spawn to launch them all, then joins in
order. Fan-out/join is the easiest parallelism there is — and exactly the
shape the capstone needs. Domain lives in the stdlib, so
plain ocamlopt builds it. The shell's time is
our stopwatch — wall-clock honesty, zero code changes:
Your numbers will differ; the shape is the lesson. The totals
are identical — same maths. real, the time you actually
waited, collapses by nearly 4× when four cores are free.
And user — CPU-seconds summed across all cores — barely
moves: the work didn't shrink, it overlapped. On a two-core
machine expect ~2×; if bench4 barely wins at all, close the
forty browser tabs currently fighting it for cores.
The lab, part 2: when counters collide
Now for the lecture every parallel programmer earns the hard way — except you get to watch it instead of debugging it at midnight. Domains share memory. So what happens when two of them increment the same counter a million times each? Two counters, kept two ways:
The plain ref loses hundreds of thousands of updates — a
different amount every run, the right amount never. Here's the crime
scene: incr plain looks like one action but is three — read
the value, add one, write it back. Two domains interleave those steps
freely: both read 41, both write 42, and one increment simply evaporates.
Atomic.incr can't lose, because the hardware performs the
whole read-modify-write as one indivisible step; there is no gap to fall
into. For counters, flags and latest-value cells, Atomic.t
is the tool. For bigger critical sections — "update these three fields
together or not at all" — the stdlib provides Mutex, a door
only one domain can hold open at a time.
The price tag
One truth before you claim the loot: domains are heavyweight.
Each one is a real OS thread carrying its own slice of the OCaml runtime.
Spawning costs real microseconds, and having many more domains than cores
just makes them queue for hardware. So the craft is: spawn few —
around Domain.recommended_domain_count (), roughly your core
count — hand each a fat slab of work, and keep them busy. A domain per
particle is a bonfire of overhead; a domain per galaxy quadrant is
engineering.
Deeper: pools, and Domainslib
Real engines don't spawn domains per task; they spawn a pool once
and feed it tasks forever. The community library Domainslib
(opam install domainslib) packages that pattern: task
pools, parallel_for, async/await over domains. It's an
external library and strictly optional reading — everything in Camel
Quest, capstone included, sticks to the stdlib: Domain,
Atomic, Mutex.
Lab log
Self-attested, as always in the lab — XP for honesty:
Checkpoint quiz
Loot collected
Domain.spawnstarts work on another core now;Domain.joincollects the result. Fan-out over independent slabs, join in order — parallelism at its easiest.timetells the story:realshrinks,userdoesn't — work overlaps instead of shrinking.- Plain
refupdates race and evaporate;Atomicis indivisible;Mutexguards bigger sections; sharing less beats locking more. - Domains are heavyweight: spawn about
recommended_domain_count, keep them busy. - The browser build can't do any of this — one JS thread; Workers pass messages instead.
You now hold every power this course promised: purity, modules, effects, parallelism. Next lesson they all report for duty on one project — the capstone.