Is calling `x = @spawn f(x)` a bad idea?

I am a bit unclear on how and when variables are captured in @spawn call. For instance, the following errors

julia> using Base.Threads

julia> let x = 1
           x = @spawn x + 3
           fetch(x)
       end
ERROR: TaskFailedException
Stacktrace:
 [1] wait
   @ ./task.jl:349 [inlined]
 [2] fetch(t::Task)
   @ Base ./task.jl:369
 [3] top-level scope
   @ ./REPL[3]:3

    nested task error: MethodError: no method matching +(::Task, ::Int64)

Although I can not find it in the current docs, I remember some example using $ when capturing variables. Trying it works

julia> let x = 1
           x = @spawn $x + 3
           fetch(x)
       end
4

I would appreciate if someone could post a reference to where the capturing logic is described. Thanks!

setup:
ubuntu, julia 1.8.1

Before any piece of code runs, macros are expanded. You can take a look at the @macroexpanded version of your code to investigate why x is captured - if I remember correctly, @spawn just expands to an anonymous function, so the capturing rules should be the same.

1 Like

The docs for @spawn mention using $ to interpolate variable values into the generated closure. If you look at the code, the macro gathers up all variables preceded by $ into a let block around the task body. (The @async macro uses the same helper function).

In general, though, I would absolutely avoid reusing the variable like you have there. You can just put the @spawn inside the fetch() call if you want:

let x = 1
  fetch(@spawn $x + 3)
end
1 Like

This still seems to be a bug, i.e., already when using closures:

julia> let x = 1; x = () -> x + 3; x() end
ERROR: MethodError: no method matching +(::var"#9#10", ::Int64)
Stacktrace:
 [1] (::var"#9#10")()
   @ Main ./REPL[46]:1
 [2] top-level scope
   @ REPL[46]:1

According to the docs, i.e., in particular let always creates a new location and *

help?> let
search: let delete! isletter deleteat! selectdim reflect! ismutabletype length

  let

  let statements create a new hard scope block and introduce new variable
  bindings each time they run. Whereas assignments might reassign a new value
  to an existing value location, let always creates a new location. This
  difference is only detectable in the case of variables that outlive their
  scope via closures. The let syntax accepts a comma-separated series of
  assignments and variable names:

  let var1 = value1, var2, var3 = value3
      code
  end

  The assignments are evaluated in order, with each right-hand side evaluated
  in the scope before the new variable on the left-hand side has been
  introduced. Therefore it makes sense to write something like let x = x,
  since the two x variables are distinct and have separate storage.

I would also expect the following to return 4 (not 6):

julia> let x = 1; f = () -> x + 3; x = 3; f() end
6

PS: It does work when using a comma separated variable list though:

julia> let x = 1, f = () -> x + 3, x = 3; f() end
4

and also the original threading example can be fixed like that

julia> let x = 1,
           x = @spawn x + 3
           fetch(x)
       end
4

I keep getting surprised by Julia’s syntax and imho that is not a good thing.

Ok, I get it now:

julia> let x = 1
           f = (y) -> x + y
           x = 3
           f(x)
       end
6

is equivalent to

let x = 1
           begin
               f = (y) -> x + y
               x = 3
               f(x)
           end
       end
6

i.e., the newline ends the let bindings and only the first variable is bound by let! If multiple variables are to be bound, they have to be separated by commas:

julia> let x = 1,
           f = (y) -> x + y,
           x = 3
           f(x)
       end
4

How did I miss that in the docs?! Thanks!