Choosing Between @threads and pmap for Parallel Loops in Julia
Deciding between @threads and pmap in Julia depends on data volume and hardware. This guide compares shared-memory threading versus distributed processing for parallel loops.
11 Nov 2025, 17:39 UTC

The Parallelism Decision: Shared Memory vs. Distributed Processes
When optimizing a compute-intensive loop in Julia, the primary technical decision is whether to use shared-memory multithreading via @threads or distributed processing via pmap. Choosing the wrong model often leads to performance degradation due to either synchronization overhead (race conditions) or serialization latency (the cost of moving data between processes).
The goal is to maximize CPU utilization while minimizing the overhead required to manage parallel tasks. For "embarrassingly parallel" workloads—where each iteration is independent—the choice depends on your data size, available hardware, and how you handle mutable state.
Comparison of Parallelism Strategies
| Feature | @threads (Base.Threads) | pmap (Distributed) |
|---|---|---|
| Execution Model | Shared-memory multithreading | Separate worker processes |
| Data Sharing | Implicit via shared heap | Explicit serialization (copying) |
| Overhead | Low (thread creation) | High (process startup, IPC) |
| Scalability | Single node (CPU cores) | Multi-node (Cluster/Network) |
| Setup | JULIA_NUM_THREADS=N |
addprocs() |
| Best Use Case | Tight loops, dense linear algebra | Large-scale data, heterogeneous tasks |
Technical Trade-offs
Shared-Memory Threading (@threads)
Multithreading is highly efficient because all threads access the same memory space. There is virtually no cost to "send" data to a thread. However, this introduces the risk of race conditions: when two threads attempt to modify the same memory location simultaneously, leading to nondeterministic results.
To avoid this, you must ensure that mutable state is either thread-local (using threadid() to index into a pre-allocated array) or protected by a lock. Threading is limited to the physical cores of a single machine and cannot scale across a network.
Distributed Processing (pmap)
The Distributed library spawns entirely separate Julia processes. Because each process has its own memory heap, race conditions are physically impossible—processes cannot modify each other's memory. This allows the workload to scale across multiple physical servers.
The trade-off is serialization overhead. Every piece of data sent to a worker and every result returned must be serialized (converted to a byte stream) and transmitted via Inter-Process Communication (IPC). For tight loops with small workloads per iteration, the time spent moving data often exceeds the time saved by parallel execution.
Implementation Example: Monte-Carlo Pi Estimation
This example demonstrates a compute-heavy loop. To avoid race conditions in @threads, we use a hits array where each thread writes to its own unique index based on its threadid().
using Base.Threads
function pi_mc_threads(n::Int)
# Pre-allocate a slot for each thread to avoid race conditions
hits = zeros(Int, nthreads())
@threads for i in 1:n
tid = threadid()
x, y = rand(), rand()
# Each thread increments its own dedicated counter
hits[tid] += (x*x + y*y < 1.0)
end
return 4.0 * sum(hits) / n
end
Execution and Configuration
To run this code, you must define the thread count before starting the Julia session. Changing thread counts at runtime is not supported in Julia 1.10+.
On Linux/macOS:
export JULIA_NUM_THREADS=4
julia script.jl
On Windows (PowerShell):
$env:JULIA_NUM_THREADS=4
julia script.jl
Alternative: Start Julia with the --threads auto flag to use all available logical cores.
Validation and Performance Check
To verify the implementation, check for both correctness and speed-up. Use the BenchmarkTools package for accurate timing.
- Correctness: Ensure the result converges to $\pi$. For $10^7$ samples, the result should satisfy $|\pi_{est} - 3.14159| < 10^{-3}$.
- Performance: Compare the median time of the serial version vs. the threaded version using
@btime.
In typical local environments (e.g., a 2-core laptop), a serial loop of $10^7$ iterations may take ~2.3s. The @threads version typically reduces this to ~1.3s (approx. 1.8x speed-up). Conversely, a pmap implementation of the same loop often takes longer (~1.9s) because the cost of managing separate processes outweighs the computational gain for this specific granularity of work.
Limitations
- @threads: Limited to a single machine; requires careful management of mutable state to prevent data corruption.
- pmap: High memory overhead per worker process; significant latency for small-task iterations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.