Multithreading and Tasks
Prerequisites: Memory, allocation, and globals
Julia can run code on several CPU cores in one process. Start Julia with threads enabled:
julia -t 4 # 4 threads
julia -t auto # one per CPU core
(or set the JULIA_NUM_THREADS environment variable; the VS Code
extension has a setting for it). The examples below ran with -t 4.
@threads
using Base.Threads
println("threads: ", nthreads())
results = zeros(Int, 8)
@threads for i in 1:8
results[i] = i^2 # each iteration writes its own slot
end
println(results)
threads: 4
[1, 4, 9, 16, 25, 36, 49, 64]
@threads splits the loop iterations among the threads. Iterations
must be independent — here each one writes a different element.
Tasks with @spawn
Threads.@spawn starts a task that may run on any thread and
returns immediately; fetch waits for its result:
using Base.Threads
function slow_sum(r)
s = 0.0
for i in r
s += sqrt(i)
end
return s
end
n = 10_000_000
chunks = Iterators.partition(1:n, n ÷ nthreads())
tasks = [Threads.@spawn slow_sum(c) for c in chunks]
parallel = sum(fetch.(tasks))
serial = slow_sum(1:n)
println(isapprox(parallel, serial), " ", round(Int, parallel))
true 21081852649
This map-reduce pattern — split the work, compute partial results independently, combine them at the end — is the safest way to use threads.
Data races
If several threads update the same variable without coordination, updates get lost:
# illustrative — the wrong total varies from run to run
using Base.Threads
total = Ref(0)
@threads for i in 1:100_000
total[] += 1 # read, add, write: threads overwrite each other
end
println(total[])
35113
Fix it with an atomic or a lock (or, better, by avoiding shared state as above):
using Base.Threads
counter = Atomic{Int}(0)
@threads for i in 1:100_000
atomic_add!(counter, 1)
end
println(counter[])
lk = ReentrantLock()
seen = Int[]
@threads for i in 1:100
iseven(i) || continue
lock(lk) do
push!(seen, i)
end
end
println(length(seen), " ", sum(seen))
100000
50 2550
Concurrency on one thread
Tasks are also useful without extra threads, for waiting on I/O:
@async starts a task on the current thread, and @sync waits for all
tasks started inside it:
order = String[]
@sync begin
@async (sleep(0.2); push!(order, "slow"))
@async (sleep(0.05); push!(order, "fast"))
push!(order, "main")
end
println(order)
["main", "fast", "slow"]
Tips
- Make sure the code is fast on one thread first.
- Threads share memory; BLAS (matrix multiplication) has its own thread
pool (
LinearAlgebra.BLAS.set_num_threads). - OhMyThreads.jl offers higher-level tools (
tmap,tforeach,treduce) with safer defaults.