Boxed Variables
All multithreading in julia is built around the idea of passing around and executing functions, but often these functions "enclose" data from an outer local scope, making them what's called a "closure".
Boxed variables causing race conditions
Julia allows functions which capture variables to re-bind those variables to different values, but doing so can cause subtle race conditions in multithreaded code.
Consider the following example:
let out = zeros(Int, 10)
Threads.@threads for i in 1:10
A = i
sleep(1/100)
out[i] = A
end
A = 1
out
end
10-element Vector{Int64}:
5
4
6
4
5
4
5
4
5
4
You may have expected that to return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
, but the nonsense result is caused by A
actually being a shared mutable container here which all the parallel tasks are accessing and mutating in parallel, giving unpredictable results.
OhMyThreads.jl tries to protect users from this surprising behaviour:
using OhMyThreads
try
let
# this throws an error!
out = tmap(1:10) do i
A = i
sleep(1/100)
A
end
A = 1
out
end
catch e;
# Show the error
Base.showerror(stdout, e)
end
Attempted to capture and modify outer local variable: A
See https://juliafolds2.github.io/OhMyThreads.jl/stable/literate/boxing/boxing/ for a fuller explanation.
Hint
----
Capturing boxed variables can be not only slow, but also cause surprising
and incorrect results.
• If you meant for these variables to be local to each loop
iteration and not depend on a variable from an outer scope, you
should mark them as local inside the closure.
• If you meant to reference a variable from the outer scope, but do
not want access to it to be boxed, you can wrap uses of it in a
let block, like e.g.
function foo(x, N)
rand(Bool) && x = 1 # This rebinding of x causes it to be boxed ...
let x = x # ... Unless we localize it here with the let block
@tasks for i in 1:N
f(x)
end
end
end
• OhMyThreads.jl provides a @localize macro that automates the above
let block, i.e. @localize x f(x) is the same as let x=x; f(x) end
• If these variables are being re-bound inside a @one_by_one or
@only_one block, consider using a mutable Ref instead of
re-binding the variable.
This error can be bypassed with the @allow_boxed_captures macro.
In this case, we could fix the race conditon by marking A
as local:
let
out = tmap(1:10) do i
local A = i # Note the use of `local`
sleep(1/100)
A
end
A = 1
out
end
10-element Vector{Int64}:
1
2
3
4
5
6
7
8
9
10
If you really desire to bypass this error, you can use the @allow_boxed_captures
macro
@allow_boxed_captures let
out = tmap(1:10) do i
A = i
sleep(1/100)
A
end
A = 1
out
end
10-element Vector{Int64}:
3
2
3
2
3
2
3
2
3
3
Non-race conditon boxed variables
Any re-binding of captured variables can cause boxing, even when that boxing isn't strictly necessary, like the following example where we do not rebind A
in the loop:
try
let A = 1
if rand(Bool)
# Rebind A, it's now boxed!
A = 2
end
@tasks for i in 1:2
@show A
end
end
catch e;
println("Yup, that errored!")
end
Yup, that errored!
This comes down to how julia parses and lowers code. To avoid this, you can use an inner let
block to localize A
to the loop:
let A = 1
if rand(Bool)
A = 2
end
let A = A # This stops A from being boxed!
@tasks for i in 1:2
@show A
end
end
end
A = 1
A = 1
OhMyThreads provides a macro @localize
to automate this process:
let A = 1
if rand(Bool)
A = 2
end
# This stops A from being boxed!
@localize A @tasks for i in 1:2
@show A
end
end
A = 2
A = 2
This page was generated using Literate.jl.