-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Boxed variables in closures passed to a Task can cause race condition #30896
Comments
The solution to this is to use
|
Ah, thanks Jeff, that makes sense. That's a working solution! :) That said, it's a bit of a bummer. It's both more complicated than the equivalent go code, and also I would think pretty non-obvious to the casual Julia user. :/ It would be nice to make spawning tasks more friendly for this kind of use case. |
Maybe another solution would be a different macro that requires its argument expression to be a Maybe like function countup()
i = 0
while i < 5
@go println(i)
i += 1
end
end
countup() Something like this, I guess: macro go(expr)
@assert expr.head == :call
f = expr.args[1]
args = expr.args[2:end]
copyargs = [gensym(string(a)) for a in args]
letargs = [:($a=$(esc(b))) for (a,b) in zip(copyargs,args)]
quote
let $(letargs...)
@async $f($(copyargs...))
end
end
end (I wouldn't actually want to call it |
That's clever; I do kind of like it. The interesting thing is that then the argument expressions are evaluated in the current task instead of in the new created task. I assume that's what go does too? Another option is to add arguments to |
Yeah, exactly. Since this is how regular function calls work (you evaluate the args before entering the function) this seems pretty natural. It's a bit counter to the way most macros behave though, so i can also see this behavior being unexpected.. For that reason it seems like a macro like this should explicitly have
I'm not a go expert (i've been learning Go only because there's a wealth of good concurrency information in Go), but my understanding is yes, that's what go does too.
Yeah, that feels reasonable, too. It's a bit awkward, but not terrible. Although that's almost starting to look like a lambda syntax again.. |
Just to add more context to this discussion, here's the exact same ideas discussed for Go: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables Their recommended solution is to pass the value as a parameter to the anonymous function, similar to what I did in my example |
I'm finally following up with this again, now that we have a multithreading I've opened #33119 to implement |
The way we spawn
Task
s via@async
can lead to a surprising race condition b/c variables inside the closure are sometimes boxed.This can make it difficult to spawn a
Task
that needs to capture a local variable.Consider this simple example:
And the problem is nothing to do with the
@async
macro itself; you'd see the same problem using the Task constructor,Task(()->println(i))
, since in both cases the behavior is related to boxing inside the closure.And since we require you to pass a zero-argument function, capturing variables with closures seems like the solution you're supposed to use.
I just checked, and
Go
's closures behave the same way. However, inGo
they skirt this problem because the syntax to launch a goroutine is required to be a function call, so you have the opportunity to pass arguments to your constructed coroutine. In go, the above example works correctly:And even if you do need to construct a closure in go, you can still pass the pass-by-copy parameters directly:
But currently we don't have a way to start a Task with a parameter. The current Task constructor requires its argument to be "callable with no arguments".
Currently the only way I can think of to mitigate this in my code is to spawn the Task with
@eval
, since it gives you the chance to splice in the value ofx
, thus forcing the pass-by-copy syntax. But that's obviously terrible for a bunch of other reasons.So I think we need some kind of alternative solution here. What do you think?
Here are a few proposals I can think of:
For
Task(f)
, allow passing a function that takes arguments.schedule(t, x,y,z)
would allow you to pass the arguments tof
, spawningf(x,y,z)
in a coroutine.schedule(t, val)
already has a definition:https://docs.julialang.org/en/v1.1.0/base/parallel/#Base.schedule
(I don't really understand how to use that, though, and the docs say use of
yieldto
"is discouraged.")We could consider allowing
@async
to use the$
interpolation syntax within its expression body to indicate pass-by-value vs pass-by-reference semantics. This would be kind of like what@btime
does.In my example, that would become:
Sadly this might be breaking, though, for any expressions that already contained quoting and interpolation inside the@async
body. 😦 (e.g.@async println(:($x + 5))
)EDIT: I no longer think this would be breaking in any cases... In the provided example, the
$
is clearly interpolating into the quote, not into@async
, following the same rules as in@eval
.We could introduce some kind of explicit capture syntax for closures that includes whether to pass-by-copy or -by-reference, maybe like that proposed here:
Explicit capture of variables in a closure. #14959 (comment)
But i tend to agree that it complicates things in an undesirable way.
Anyone have any other ideas?
EDIT: Also, I opened this discourse thread to get more background info on why the boxing is needed 🙂:
https://discourse.julialang.org/t/surprising-capture-boxing-behavior-in-closure/20254
If you're interested, I came across this behavior when translating this cool prime number sieve written in Go into Julia:
http://tinyurl.com/gosieve
Here's the julia code I wrote:
The text was updated successfully, but these errors were encountered: