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…

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

You now hold every power this course promised: purity, modules, effects, parallelism. Next lesson they all report for duty on one project — the capstone.