julia> Threads.nthreads()
4
julia> my_lock = Threads.ReentrantLock()
ReentrantLock(nothing, 0x00000000, 0x00, Base.GenericCondition{Base.Threads.SpinLock}(Base.IntrusiveLinkedList{Task}(nothing, nothing), Base.Threads.SpinLock(0)), (0, 0, 0))
julia> function my_dumb_fun(af)
end;
julia> for k = 1:1
Threads.@threads for i = 1:3
a = -10i
my_dumb_fun() do
a = 10i
end
println("at the end of i = $i, a = $a")
end
end
at the end of i = 1, a = -10
at the end of i = 3, a = -30
at the end of i = 2, a = -20
julia> for k = 1:1
Threads.@threads for i = 1:3
a = -10i
Threads.lock(my_lock) do
a = 10i
end
println("at the end of i = $i, a = $a")
end
end
at the end of i = 1, a = 10
at the end of i = 3, a = 30
at the end of i = 2, a = 20
Yes, it will be different.
But my point was also on the scope rule of do block. Isn’t it a hard local scope so that the inner a will not affect the outer a?
julia> function my_dumb_fun(af)
af()
end;
julia>
julia> for k = 1:1
for i = 1:3
a = -10i
my_dumb_fun() do
a = 10i
end
println("at the end of i = $i, a = $a")
end
end
at the end of i = 1, a = 10
at the end of i = 2, a = 20
at the end of i = 3, a = 30
This is actually one of the few things I strongly dislike with julia. If you happen to have a variable a outside the @threads loop, and use a inside, all the parallel tasks share the same a. It’s relatively harmless if a is only read inside the loop, and quite nice for setting up constants and whatever, but it is devastating if a is used as a temporary variable which is assigned to in the parallel tasks.
But here is one immediate question—why doesn’t this work Well, maybe don’t need to ask
julia> function my_dumb_fun(af)
af()
end;
julia> function someuniquename()
a = 10i
end;
julia> for _ = 1:1
for i = 1:3
a = -10i
my_dumb_fun(someuniquename)
println("at the end of i = $i, a = $a")
end
end
ERROR: UndefVarError: `i` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
Stacktrace:
[1] someuniquename()
@ Main .\REPL[2]:2
[2] my_dumb_fun
@ .\REPL[1]:2 [inlined]
[3] top-level scope
@ .\REPL[3]:4
To be honest, I am greatly shocked. Look at this image
My thought was aligned with ChatGPT’s, which is incorrect. I just realized that these are different
julia> for i = 1:3
a = -10i
let
a = 10i
end
println("a = $a")
end
a = 10
a = 20
a = 30
julia> for i = 1:3
a = -10i
let a = 10i
a = 1a
end
println("a = $a")
end
a = -10
a = -20
a = -30
So a let is very unlike a function, as shown in the follows
julia> for i = 1:2
a = -10i
let
a = 10i
end
println("a = $a")
end
a = 10
a = 20
julia> function my_let(i)
a = 10i
end;
julia> for i = 1:2
a = -10i
my_let(i)
println("a = $a")
end
a = -10
a = -20
I thought that they were the same.
Therefore I don’t think it is appropriate to make an analogy between do and function blocks, just like between let and functions. What’s your opinion?
Writing an a = 10i in the body of a let block, versus writing the same a = 10i in the body of a function, is very unlike.
Your example with the function my_let is in the REPL, i.e. at the julia prompt. The scope rules are somewhat different there. The function my_let will not look in the global scope to find the a when you assign to it, only in outer local scopes. For that to work, you would need global a = 10i.
Since the scoping rules are somewhat different at the REPL (outer level), it is good practice to enclose all code in functions.
julia> a = 12
12
julia> function fun()
function my_let(i)
a = 10i
end
for i = 1:2
a = -10i
my_let(i)
println("a = $a")
end
end
fun (generic function with 1 method)
julia> function fun2()
function my_let(i)
a = 10i
end
for i = 1:2
a = -10i
my_let(i)
println("a = $a")
end
a = 42
return nothing
end
fun2 (generic function with 1 method)
julia> fun()
a = -10
a = -20
julia> fun2()
a = 10
a = 20
julia> a
12
In function fun, the my_let assigns to a, but there is no a in the outer scope (inside fun), because the for loop creates its own scope, so the a in my_let is local, and different from the one in the loop.
In function fun2, there is an a in the outer scope (the a = 42 at the end), so the a in my_let and inside the loop is the same. It’s a bit contrived with the a = 42 at the end, but imagine you write such a function, and you have various loops and functions which all are supposed to act on a. It would work just as expected.
In my applications, I would typically work in the julia REPL, meaning that I would not purposefully wrap all my code within a function, since in that case debugging is inconvenient. Therefore in the preceding discussions you see my writing a dumb for k = 1:1 loop at the outermost layer.
Even considering only the julia REPL context, the behavior of the let is rocket science! I assume that it is very hard for users to predict whether the a = 10i written within the body of let can modify an outer a.
julia> let
a = 0
end;
julia> @isdefined a
false
julia> for i = 1:2
a = -10i
let # can modify outer
a = 10i
end
println("i = $i, a = $a")
end
i = 1, a = 10
i = 2, a = 20
julia> @isdefined a
false
julia> a = -10;
julia> let i = 1 # cannot modify outer
a = 10i
end;
julia> a
-10
julia> for _ = 1:1
for i = 1:2
a = -10i
let # cannot modify outer
a = 10i
end
println("i = $i, a = $a")
end
end
i = 1, a = -10
i = 2, a = -20
I assume that the body of do are almost the same as the let case. Am I right?
The head line of do and let both introduce local variables—I just realize it today.
Note that in addition to some annoying scoping rules, there’s another drawback with working at the REPL. Julia’s unit of compilation is the function. If your code is not in a function, it will not be compiled, but interpreted. Which means it will be slower by 1-3 orders of magnitude.
julia> const n = 100000000
100000000
julia> @time "repl" begin
s = 0.0
for i in 1:n
s += 1/i
end
s
end
repl: 5.050498 seconds (200.00 M allocations: 2.980 GiB, 1.69% gc time)
18.997896413852555
julia> function sumthem(n)
s = 0.0
for i in 1:n
s += 1/i
end
return s
end
sumthem (generic function with 1 method)
julia> @time "function" sumthem(n)
function: 0.100164 seconds
18.997896413852555
julia> s = 0;
julia> for i = 1:3
s = i
end
julia> s # 3
3
julia> s = 0;
julia> Threads.@threads for i = 1:3
s = i # maybe this usage is wrong
end
julia> s # 0
0
So you make judgements with the source code of my my_dumb_fun. This is to say, the scope inference about the do block is much more difficult. (I am not aware what the source code of the Threads.lock function is).
Therefore, if the circumstance is like this, I cannot make a deterministic inference
julia> function modify_a_randomly(af)
rand() > .5 && return
af()
end;
julia> for i = 1:5
a = -10i
modify_a_randomly() do
a = 10i
end
println("i = $i, a = $a")
end
i = 1, a = 10
i = 2, a = -20
i = 3, a = 30
i = 4, a = -40
i = 5, a = 50
function fun()
s = 0
Threads.@threads for i = 1:3
s = i
end
return s
end
julia> fun()
3
The content of the @threads loop is put inside an anonymous function by the @threads macro. It will look for an s in an outer local scope. The s in the function is local, so it will be used.
On the other hand, when the @threads loop is defined at top-level, the outer scope with the s is global, so it will not be looked up.
The for loop without @threads is not in a function, it creates a soft scope, and works differently at the REPL.
There’s a lengthy explanation in Scope of Variables · The Julia Language.
There are good reasons for the put code in functions advice.