Stream: helpdesk (published)

Topic: Safety of local variables inside `@spawn`


view this post on Zulip Kenta Sato (May 27 2021 at 08:29):

The following code is unsafe (or the result is undefined) in general because the loop variable i may be changed when a new task is spawned and run:

using .Threads: @spawn
function loop(n)
    tasks = Task[]
    @sync for i in 1:n
        push!(tasks, @spawn i)
    end
    return sum(fetch, tasks)
end

Instead, we should interpolate the variable with $ to copy the value, right?

view this post on Zulip Takafumi Arakaki (tkf) (May 27 2021 at 10:03):

You don't need interpolation in this case because the scope of i is inside the loop and i is never re-assigned. You'd have the problem if you are capturing variables that are re-assigned. For example:

julia> let tasks = []
           a = 0
           @sync for i in 1:3
               a += i
               push!(tasks, Threads.@spawn (i, a))
           end
           fetch.(tasks)
       end
3-element Vector{Tuple{Int64, Int64}}:
 (1, 6)
 (2, 6)
 (3, 6)

Using $a (or let a = a ... end) is a good solution here.

view this post on Zulip Kenta Sato (May 27 2021 at 10:55):

Thank you! So, we can think i as a variable that is newly generated for each iteration and therefore iterations have its own isolated variable. I need to update my mental model.

view this post on Zulip Takafumi Arakaki (tkf) (May 27 2021 at 22:29):

Exactly! This is actually how closures work in Julia. Task-creating macros like @spawn are just a thin syntax sugar on top of closures.

I think it's easier to play with closures and figure out how it interacts with the scoping rule:

julia> fs = []
       for i in 1:3
           push!(fs, () -> i)
       end
       [f() for f in fs]
3-element Vector{Int64}:
 1
 2
 3

Last updated: Oct 02 2023 at 04:34 UTC