Threads.@threads and Threads.@spawn

Hi everyone,

I have a short question regarding the usage of the Threads.@spawn macro in combination with Threads.@threads. Say I have code which looks like

some_task = Threads.@spawn begin 
        some_function_call_1()
        some_function_call_2() 
end 

for rep in reps

  #this is the main loop
  Threads.@threads for iter in iters
         do_main_work(iter)
  end

  wait(some_task)

  some_task = Threads.@spawn begin 
         some_function_call_1()
         some_function_call_2() 
  end 

end

I my naive understanding, the code does the following:

1.) Initializes some_task on one available thread
2.) Enters the reps loop
3.) Parallelize main work load (the inner loop) among available threads
4.) Wait for ‘some_task’ to finish
5.) Run some_task on one available thread.
6.) Continue with next iteration in reps loop with some_task still running

I have a few questions regarding this
1.) Does this code actually do what I think it does?
2.) If the respective thread is finished with some_task and the iters loop is still running, will the remaining workload be dynamically managed such that this thread does not become idle and can steal some work from the other threads?

I am using the current master branch for this.

Thanks in advance.

This runs:

using Base.Threads

some_function_call_1() = begin
    println("some_function_call_1, ", Threads.threadid())
    sleep(rand())
end
some_function_call_2() = begin
    println("some_function_call_2, ", Threads.threadid())
    sleep(rand())
end
do_main_work(iter) = begin
    println("do_main_work, $(iter), ", Threads.threadid())
    sleep(rand())
end

some_task = Threads.@spawn begin 
    some_function_call_1()
    some_function_call_2() 
end 

for rep in 1:3
    Threads.@threads for iter in 1:4
        do_main_work(iter)
    end
    global some_task
    wait(some_task)
    some_task = Threads.@spawn begin 
        some_function_call_1()
        some_function_call_2() 
    end 
end

wait(some_task)

The output is

julia> include("c:/cygwin64/home/PK/thread_buffers.jl");
some_function_call_1, 2
do_main_work, 1, 1
do_main_work, 3, 2
do_main_work, 4, 2
some_function_call_2, 2
do_main_work, 2, 1
do_main_work, 1, 1
some_function_call_1, 2
do_main_work, 3, 2
do_main_work, 2, 1
some_function_call_2, 2
do_main_work, 4, 2
some_function_call_1, 2
do_main_work, 3, 2
do_main_work, 1, 1
do_main_work, 2, 1
some_function_call_2, 2
do_main_work, 4, 2
some_function_call_1, 2
some_function_call_2, 2
4 Likes

Many thanks for your answer. However, is there any reason why some_task is declared a global inside the rep loop (it has been initialized already in the outer scope)?

This has to do with the global scope in which the whole thing is written. Compare with


using Base.Threads

some_function_call_1() = begin
    println("some_function_call_1, ", Threads.threadid())
    sleep(rand())
end
some_function_call_2() = begin
    println("some_function_call_2, ", Threads.threadid())
    sleep(rand())
end
do_main_work(iter) = begin
    println("do_main_work, $(iter), ", Threads.threadid())
    sleep(rand())
end

function fun()
    some_task = Threads.@spawn begin 
        some_function_call_1()
        some_function_call_2() 
    end 

    for rep in 1:3
        Threads.@threads for iter in 1:4
            do_main_work(iter)
        end
        wait(some_task)
        some_task = Threads.@spawn begin 
            some_function_call_1()
            some_function_call_2() 
        end 
    end

    wait(some_task)

end

fun()

where the loops are inside function written for that purpose.

1 Like

Sure. Should have mentioned that the function is wrapped into a function in the actual code.