Skip to content
AgileLogics
7 min read

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.