OCaml 5 Domains: Parallel CPU Work with Domain.spawn and Mutex
Split CPU-bound work across OCaml 5 domains using Domain.spawn and Mutex, with a worked example and GC caveats.
01 Mar 2026, 12:23 UTC

You have a pure function over a two-million-element array, and your eight-core laptop is using one CPU core. The program is correct, but it is sequential. OCaml 5 introduced domains: multiple OCaml execution contexts that run in parallel inside a single process and share memory. This post explains when to use domains for CPU-bound work, how to protect shared results with Mutex, and why I/O-bound work usually stays on one domain with Lwt, Async, or Eio.
Domains are for CPU-bound parallelism, not I/O concurrency
A domain is an OCaml execution context with its own minor heap, scheduled by the runtime on an operating-system thread. Domains in the same process share memory, which makes them suitable for splitting a computation across CPU cores. Domain.recommend () returns a domain count based on detected cores, and Domain.spawn starts a new domain running a function you provide.
Domains are not a replacement for async concurrency. If your program spends most of its time waiting on network sockets or file descriptors, Lwt, Async, or Eio on a single domain is usually the better fit. Domains shine when the work is CPU-bound: numeric processing, image transformations, compiling, or large reductions over immutable data.
Worked example: parallel sum with shared state
This example splits a float array into chunks, sends each chunk to its own domain, and adds each chunk’s subtotal to a shared total protected by a Mutex. A Condition lets the main domain wait until every worker reports in.
First, confirm you are on OCaml 5 or later. Run this in your terminal:
ocaml -version
# In the toplevel:
# Sys.ocaml_version;;
You need OCaml 5.0 or higher. The Domain module does not exist in OCaml 4.x.
Save the following as par_sum.ml. Compile it with ocamlfind (or dune) from a normal user shell; no special permissions are required:
ocamlfind ocamlopt -package unix -linkpkg par_sum.ml -o par_sum
(* par_sum.ml: OCaml 5.0+ *)
let heavy_sum arr offset len =
let acc = ref 0.0 in
for i = offset to offset + len - 1 do
acc := !acc +. arr.(i) *. arr.(i)
done;
!acc
let par_sum arr =
let n = Array.length arr in
let domain_count = max 1 (Domain.recommend ()) in
let chunk = (n + domain_count - 1) / domain_count in
let total = ref 0.0 in
let remaining = ref domain_count in
let lock = Mutex.create () in
let all_done = Condition.create () in
let rec spawn_from start =
if start >= n then ()
else
let finish = min n (start + chunk) in
Domain.spawn (fun () ->
let subtotal = heavy_sum arr start (finish - start) in
Mutex.lock lock;
total := !total +. subtotal;
remaining := !remaining - 1;
if !remaining = 0 then Condition.signal all_done;
Mutex.unlock lock
);
spawn_from (start + chunk)
in
spawn_from 0;
Mutex.lock lock;
while !remaining > 0 do
Condition.wait all_done lock
done;
Mutex.unlock lock;
!total
let () =
let data = Array.init 1_000_000 (fun i -> float i) in
let result = par_sum data in
Printf.printf "Parallel sum: %f\n" result
Where to run: a terminal on a multi-core host. Required permissions: none beyond ordinary file access. Placeholders: replace par_sum.ml with your source path. Expected checks: the program should print a finite sum matching a sequential loop, and Sys.ocaml_version should report a major version of 5 or higher. Risks: compiling with OCaml 4.x fails because Domain is unavailable; sharing total or remaining without Mutex.lock is a data race; exceptions raised inside Domain.spawn do not propagate to the parent, so worker functions should catch and report their own errors.
Synchronization, garbage collection, and limits
The standard library’s Mutex and Condition modules synchronize shared mutable state across domains. Prefer immutable values where possible; lock only what you must. The garbage collector runs per-domain with local minor collections, but major collections are stop-the-world across all domains, which can create latency spikes under heavy allocation.
Spawning more domains than physical cores can oversubscribe the CPU and reduce throughput because of context switching and GC synchronization. Parallel speedup is not guaranteed for memory-bandwidth-bound or allocation-heavy programs.
Practical check before you adopt domains
Benchmark the specific workload. Measure wall-clock time with Unix.gettimeofday before and after a sequential implementation, then compare it to the domain-based version. If the parallel version is not meaningfully faster, or if allocation dominates runtime, stay sequential or reconsider the algorithm.
Actionable closing: verify Sys.ocaml_version is 5.x, call Domain.recommend () to size your worker pool, keep I/O on async libraries, and protect every piece of shared mutable state with Mutex. Profile first; domains are a tool for CPU-bound parallelism, not a default speedup.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.