I have a function which contains a loop which should normally be run multi threaded using Threads.@threads, but sometimes I want to run it single threaded.
function foo(multi_thread=true)
mt = multi_thread ? Threads.@threads : nothing
mt for ii in 1:10
@show ii
end
end
It is valid for mt to be nothing here, but an error occurs when trying to set mt=Threads.@threads.
This is not going to work, because the @threads macro expands at parse time.
You can however define a custom macroThis is mostly useless, because the value of flag mt must be known at parse time too.
julia> macro withthreads(mt::Bool, ex)
if mt
return esc(:(Threads.@threads $ex))
else
return esc(ex)
end
end
@withthreads (macro with 1 method)
julia> @withthreads true for i in 1:5
println(Threads.threadid())
end
1
1
2
4
3
julia> @withthreads false for i in 1:5
println(Threads.threadid())
end
1
1
1
1
1
I figured it must be something like that as it gave an error about passing : as an arg.
Thanks for your solution, defining a custom macro seems somewhat unnecessary and potentially confusing to users. It’s a shame there isn’t a way of having a short if statement in the function foo
Basically instead of OP’s foo(multi_thread=true) attempting to modify its own code at runtime (impossible with a macro), foo(mt) created both versions of the code at parse-time and selects the branch at runtime. Turning a single-threaded version into foo(mt) could also be done with a macro, but it would only save effort if this pattern needs to be applied to several methods.
My only other thought is that dispatch can be leveraged if this was split into two different methods instead of two branches; one method could have an extra singleton argument e.g. foo(...) vs foo(::OneThread, ...). This dispatch could happen at runtime as designed, but it could also happen at compile-time if there were a chain of single-or-multi-threaded functions calling one another. But I don’t think there would be any significant gain because the multi_thread/mt check is cheap and outside of multithread-able loops (you want each iteration to be its own thread, so no need to check).
I’d rather avoid multiple dispatch as the contents of the for loop is identical, so this would lead to code duplication. If I make the entire for loop contents a function there would still be a few lines duplicated and in my case the readability of the code would be impacted.
@skleinbo Thanks for your answer. As suggested above _foo hurts code readability and we have some duplicated lines here. Perhaps Julia isn’t able to get around these shortcomings in this instance though.
If no one else suggests an idea which overcomes this, I’ll select your solution
Your concern about code duplication is valid, but there’s a bit of nuance here. Single-threaded and multi-threaded loops work very differently, so the code has to be different on some level. @threads is easy to type in front of a loop, but it’s doing a lot to the loop’s code at parse-time, just dig into its source code and see. The source code however remains the same to you, and you maintain it the same way as if it weren’t multithreaded.
Similarly, you could accomplish @skleinbo did with foo(mt) but by applying a macro to a loop, looking something like:
function foo(multi_thread=true)
@multi_single_branch for ii in 1:10
@show ii
end
end
This way, you can have your branched code at parse-time but don’t need to maintain duplicated code in the source.
You are probably right about eschewing multimethods; besides there being little gain, it seems like it’ll need a more complicated macro than making a loop.
I suggest that it is probably worth using one of the packages that provide more detailed threading functionality, and expose it as functions. Such as ThreadsX.jl
The @threads macro in particular is kind of a legacy from before Julia had first class, user extensible, threading.
It has some advantages but not a lot.
(Julia, like LaTeX and unlike say C# or Python; is absolutely not batteries included. Many of the best things live in packages. But julia’s package manager makes it easy to install packages and makes their compat safe.)
With ThreadX.jl you can do:
using ThreadsX
function foo(multi_thread=true)
_foreach = multi_thread ? ThreadsX.foreach : Base.foreach
_foreach(1:10) do ii
@show ii
end
end
which works:
julia> foo(false)
ii = 1
ii = 2
ii = 3
ii = 4
ii = 5
ii = 6
ii = 7
ii = 8
ii = 9
ii = 10
julia> foo(true)
ii = 1
ii = 9
ii = 6
ii = 3
ii = 7
ii = 8
ii = 10
ii = 5
ii = 2
ii = 4
Thanks, this is exactly what I was looking for, it allows for the implementation without code duplication and doesn’t make the code much more complex to follow. I’ll use ThreadsX and the related FLoops.jl from now on.
With FLoops.jl you can pass differente “executors” as arguments that control if the code if run serially of in parallel. More details here: Parallel loops · FLoops