Choosing Between Base.Threads and Distributed.jl for CPU‑Bound Julia Workloads
When speeding up a CPU‑bound Julia program, decide whether to use shared‑memory <code>Base.Threads</code> or multi‑process <code>Distributed.jl</code>. This guide compares constraints, trade‑offs, and shows concrete code for both models.
13 Mar 2026, 19:39 UTC

Decision
When you need to speed up a CPU‑bound Julia program, decide early whether to use Base.Threads (shared‑memory multithreading on a single machine) or Distributed.jl (multi‑process parallelism that can span one or many machines). The choice hinges on the execution environment, the need for isolation, and the cost of inter‑process communication.
Constraints
- Base.Threads requires the Julia process to be started with a thread count:
julia --threads=Nor the environment variableJULIA_NUM_THREADS. The value is fixed for the lifetime of the process. - Distributed.jl needs a Julia binary on every remote host and a way to launch workers, typically via
addprocsor amachinefile. It can be used on a single machine with multiple processes or across a cluster. - Both approaches assume the workload is CPU‑bound and that the data can be partitioned without excessive cross‑dependency.
- Shared‑memory threads cannot be used for data that must remain isolated between workers (e.g., when using external libraries that are not thread‑safe).
Supported Options
| Feature | Base.Threads | Distributed.jl |
|---|---|---|
| Execution model | Single process, multiple OS threads | Multiple Julia processes, each with its own heap |
| Memory sharing | Full shared memory | No shared memory; requires serialization |
| Startup overhead | Minimal (process already running) | Process launch and worker initialization |
| Inter‑process cost | None (direct memory access) | Serialization/communication via sockets or MPI |
| Isolation | None (threads share all global state) | Full isolation; safer for non‑thread‑safe libraries |
| Scalability | Limited to physical cores on one machine | Scales to many machines in a cluster |
| Typical use case | Fine‑grained CPU work on a single node | Large data or cluster‑level parallelism |
Trade‑Offs
Base.Threads offers low latency and no serialization, but you must guard against data races. The @threads macro does not preserve iteration order, so any reduction that writes to a shared variable needs atomic operations or locks. Also, each thread must only modify its own slice of data to avoid false sharing.
Distributed.jl eliminates the risk of race conditions because each worker has its own memory space. However, every argument and return value is serialized, which can dominate the runtime for fine‑grained tasks. The overhead of launching workers and establishing communication can also reduce speedup for small problems.
Concrete Implementation
Suppose you have a vector data and a CPU‑heavy function f that takes a single element and returns a result. The following shows how to parallelize with each approach.
Using Base.Threads
# Run Julia with at least 4 threads: julia --threads=4
using Base.Threads
function parallel_map(f, data)
results = Vector{eltype(f(data[1]))}(undef, length(data))
@threads for i in 1:length(data)
results[i] = f(data[i]) # each thread writes only its index
end
return results
end
Key points:
- Preallocate
resultsto avoid dynamic resizing. - Each thread writes to a unique index; no locks are needed.
- Iteration order of
@threadsis nondeterministic, but the output vector preserves order because indices are fixed.
Using Distributed.jl
# Start Julia and add workers (local or remote)
using Distributed
addprocs(2) # or use addprocs(2, dir="/path/to/julia") for remote nodes
@everywhere function f(x)
# CPU‑heavy computation
end
function distributed_map(f, data)
# pmap splits the data into chunks and distributes them
return pmap(f, data)
end
With pmap, each worker receives a chunk of the input vector, applies f, and returns the results. The framework serializes data and f to each worker, so overhead is higher than with threads.
Validation & Benchmarking
Measure the speedup relative to a serial baseline to decide which model works best for your workload.
- Serial baseline:
using BenchmarkTools @btime map(f, data) - Threaded version:
@btime parallel_map(f, data) - Distributed version:
@btime distributed_map(f, data)
When the per‑iteration work is large (e.g., a multi‑second matrix multiplication), Threads.@threads should approach linear speedup up to the number of physical cores. If the workload is small or you need to run on a cluster, Distributed.jl may be preferable despite serialization costs.
Limitations & Practical Checks
- Thread safety: Verify that
fdoes not modify global state or shared mutable objects. Useisthreadsafe(f)(if available) or manual inspection. - False sharing: Ensure that array elements written by different threads are not adjacent in memory when the data type is small (e.g.,
Int8). Padding or using larger blocks can mitigate this. - Process isolation: For code that loads external libraries (e.g., BLAS with thread‑unsafe wrappers), prefer
Distributed.jlto avoid interference. - Check that the Julia binary on each remote host matches the main process (same Julia version and package environment) to avoid serialization errors.
Conclusion
Use Base.Threads when you have a single machine with multiple cores, the workload is embarrassingly parallel, and you can guarantee no data races. Opt for Distributed.jl when you need isolation, plan to scale across nodes, or when the cost of serialization is outweighed by the ability to process larger data sets or run on a cluster.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.