Difference b/w @threads and @spawn macros

What the difference b/w @threads and @spawn? It’s mentioned somewhere that @spawn spawns just one task thread. If so then how it can be different than @threads?

1 Like

Have you read the manual and the docstrings of the macros? If so, what’s unclear?

You might also take a look at this: JuliaHLRS22/4_multithreading.ipynb at main · carstenbauer/JuliaHLRS22 · GitHub

1 Like

@carstenbauer How these two are different?

@threads for i in 1:2*Threads.nthreads()
println("Hi, I’m ", Threads.threadid())
end

for i in 1:2*Threads.nthreads()
@spawn println("Hi, I’m ", Threads.threadid())
end

One difference:

begin 
    Threads.@threads for i in 1:2*Threads.nthreads()
        println("Hi, I’m ", Threads.threadid())
    end
        
    println("after loop")
end

returns

Hi, I’m 3
Hi, I’m 2
Hi, I’m 1
Hi, I’m 1
Hi, I’m 2
Hi, I’m 2
Hi, I’m 4
Hi, I’m 4
Hi, I’m 3
Hi, I’m 3
after loop

whereas

begin 
    
    for i in 1:2*Threads.nthreads()
        Threads.@spawn println("Hi, I’m ", Threads.threadid())
    end
    println("after loop")
end

returns the following, in this order:

after loop
Hi, I’m 1
Hi, I’m 3
Hi, I’m 3
Hi, I’m 2
Hi, I’m 3
Hi, I’m 2
Hi, I’m 4
Hi, I’m 2

As you see, the code with @spawn first printed after loop, (at least this time).
So why is that? To put it short, @spawn off-loads whatever task comes after it (so here the printing direction) to a newly spawned thread on whatever core is available right now and then immediately moves on.
So in this case, every time the loop hits @spawn, it tells one of the cores to go and print something and then while these cores are doing their thing it moves on and prints after loop.

@threads is much more restrictive, essentially you always place it in front of a loop and then it schedules a bunch of tasks and assigns them to the cores (In newer Julia versions this scheduling can also be dynamic as in the previous case). More importantly, it only parallelizes over a loop and waits until every element of the loop is properly finished before it allows the main process to continue.

@spawn allows for a bit more fine-tuning. You can spawn threads outside a loop, or within a nested loop:

begin
    Threads.@spawn println(1)
    Threads.@spawn println(2)
end
# 2
# 1


Threads.@sync begin
    for i in 1:3
        for j in 1:2
            Threads.@spawn println((i,j))
        end
    end
end
#(1, 1)
#(2, 2)
#(3, 2)
#(3, 1)
#(2, 1)

By the way the @sync in the last example is to syncronize all the task, i.e. to wait for all processes to finish before returning (In this case a bit pointless but often something you want to do before returning results).

I think since @threads can see “ahead” how many tasks need to be created, it probably has a bit less overhead (?) but I’m not sure to be honest. I think as a rule of thumb, if you have a loop that you want to parallelize over, its best to use @threads, if you need to fine-tune a bit more in how exactly the tasks are created, @spawn is best.

2 Likes

See what @Salmon said above (@threads is “blocking”, @spawn is not).

Yes, @threads looks at the iteration range and, depending on the scheduling scheme, decides how to map this iteration range to a bunch of tasks. For example, based on the number of available Julia threads, @threads :static splits up the iteration range evenly, creates precisely one task per thread, and schedules the tasks statically (first thread gets the first task, second the second and so on). @threads :dynamic (default) on the other hand creates O(numthreads) many tasks (how many exactly is an implementation detail) and schedules them dynamically (this has a bit of overhead but has the advantage that it allows for composable multithreading for example). The @spawn version creates “length of iteration range” many tasks, because it creates one task per iteration and scheduling is dynamic as well. Depending on how many threads there are and how long the iteration range is this will have higher overhead(creation and dynamic scheduling of O(numthreads) vs “length of iteration range” many tasks).

(BTW, this is also (to some extent) explained in the material I linked above)