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?
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
@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.
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)