-
-
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
Race condition caused by variable scope getting lifted from a multithreaded context #14948
Comments
Hmm, without looking into the detail, my guess is that the assignment to |
i.e. something like function wrong1(v)
ws = ntuple(w->Vector{Int}(length(v)), nthreads())
@threads for t in 1:nthreads()
for i in 1:length(v)
w = ws[t]
w2 = w
w2[i] = v[i]
end
end
w = 1 # unused binding to the name of the previously used loop variable
return ws
end vs function wrong2(v)
ws = ntuple(w->Vector{Int}(length(v)), nthreads())
lock = SpinLock()
@threads for t in 1:nthreads()
for i in 1:length(v)
lock!(lock)
w = ws[t]
w2 = w
unlock!(lock)
w2[i] = v[i]
end
end
w = 1 # unused binding to the name of the previously used loop variable
return ws
end |
The lock fixes it, I think you're right about the scope lifting of |
@threads
If we want the behavior to be as close as possible to a non-threading version, we could make all assignments in a This doesn't help if the user is calling another closure that assigning to another local variable in the outer scope but I don't think we can fix that without completely break the scoping rule. |
This is actually what the hacky |
You do want to be able to access shared variables in an outer scope from inside a parallel loop; it's a simple and useful pattern. I don't think we can force local-only assignments. I think the right answer is to generate a warning when a variable's scope is lifted after there has been an assignment to it. Practical? |
What do you mean by "local-only assignments"? For example, I'm not sure what this code is meant to do:
In this case, why is it useful to share |
And I think this can be done with sth like, x = Ref(0)
@threads for i in 1:n
f(i) && x[] = 1
end even if we "localize" all the variables in a |
By "local-only assignments", I was referring to @yuyichao's suggestion to make all assignments in an
Or:
|
Both of the code you have does not have assignment in the loop. There's mutations of mutable object but no assignment so they won't be affected at all. |
Given
The principle is the same. |
This pattern can be covered with |
Sure. There are all sorts of ways to get those semantics, but are they clear semantics? My feeling is not. |
What's not clear about this? I think allowing assignment to local variable by default is not very clear given the example in the original post. Raising a warning based on some guessing doesn't seem too nice either since the actual problem is not about where the variable is defined in the enclosing scope but that there's code that works with single threaded (that reuses a variable in the enclosing scope) but not with multi-threading. The alternative schematic is that you can't assign to a local variable in a |
To me, the following two snippets are inconsistent.
If there is a well-understood difference between assignment to a variable and mutation via a call among Julia programmers, then I guess this is just me. Still, the other thing to consider is that the serial elision of the first loop (i.e. remove just |
I think this is a very fundamental distinction in the schematics.
It doesn't have too. Same with the behavior on a non-threading build. |
Not sure what you mean? The localization approach does mean that the behavior of the code will be different if you run the loop serially vs. if you run it in parallel. One of the nicest things about Cilk is serial equivalence (https://software.intel.com/en-us/blogs/2012/04/07/serial-equivalence-of-cilk-plus-programs). I'm not sure we actually have it with Julia threads at the moment, but it is a worthwhile goal. |
I think I agree with @kpamnany here; with threads it is possible and efficient to keep the same scope behavior we have now. The only downside is race conditions, but those are possible anyway with assignment to arrays. I think the only reason an issue was filed here is that a variable can be added to a scope by an assignment that occurs after a use of the variable. I can see why this is confusing, but it's what we've always done. It tends to make a lot of sense in a case like
|
No, it means that the behavior is different when
I don't agree. There can be code like, # <...>
x = ... # this is as likely to happen as if x is used as a temporary variable after the loop
# <...>
@threads for ...
# <...>
x = ...
# <...>
end
# don't care about the value of x here The code uses |
Yes, it's really easy to write parallel code with race conditions. Nothing new there. |
Sure. I'm fine if we just say this is a user error and we will do nothing about it (and the user should add I just don't think the problem is the order and we shouldn't be adding a warning for such case. |
I'm very glad to hear that, because I don't really want to change this :) |
Can you explain this please?
In the absence of understanding of the above, I still think that lifting a variable's scope after use of that variable (lexically speaking) should cause a warning. There's no guesswork in such a rule, but I guess there are other priorities. A race condition checker should be on the TODO list somewhere. :-/ |
@kpamnany In that code |
Dont use @threads onless you want me in on this guys... On Sat, Feb 6, 2016, 03:16 Jeff Bezanson [email protected] wrote:
|
@threads Sorry, I guess I forgot to quote it in code block when quoting the reply ... |
Why does the code following We can of course keep the syntax, but we can define that the internal representation corresponds to a lambda expression, so that all local variable assignments remain hidden. This transformation can still be done when multi-threading is disabled; it's actually probably useful for SIMD-vectorization etc. as well. |
it is lowered to a lambda and has the exact same scoping behavior. whether this behavior (mutating closed over variables) is a good idea is another question. I think in some cases you do want that. |
Just to add my 2c. I think should warn or even prevent on access to shared variable or overlapping array ranges in different threads or any parallel code. If you can deduce outer scope access is mutually exclusive or race condition free by using a lock or CAS operation then only it compile normally. Also the same checking infrastructure can be used in other parallelization constructs. |
This problem (deciding dynamic array ranges overlap) is undecidable. Worse, it's hard for all but the simplest cases. You get to pick between a deluge of false positives or a safety feature that almost never triggers. The solution to this issue must be a consistent rule. It's fine for SMP programming to be hard if you want to do it manually, let's just find a way to avoid mistakes that are avoidable, such as the one in this issue, rather than making a full blown static race condition checker :-) |
What every works. Languages like Nim (http://nim-lang.org/) uses a proof assistant to check these conditions. Having said this as you say perhaps you can start with the most common and gradually increase what concurrency errors you might get perhaps using a formal approach so one day nearly every case is checked. |
I think the conclusion here is that this is intended behavior and there are no current plans to change it. If so, are you happy to close this @jrevels? |
The consensus here makes sense to me, but I could see this issue being a common pitfall for new users, so I'd like to make sure that it eventually gets a mention in our multithreading documentation (which doesn't yet exist, but I'm assuming will one day once multithreading has stabilized). I could close this and open a new docs issue, if people prefer, otherwise I'll just leave this issue open. |
More help from the compiler with an error message when there is a definitive race condition and a warning when there is a potential race condition would be the best way forward. You can add it to the docs but best is to know this when you are compiling and there is a issue with the code. |
Closing since this is by-design and fixed by resolving the race-condition. |
Combined with our assignment rules in if use_threads
@threads for i = 1:nthreads()
Xlocal = copy(X) # because I want a thread local copy of X
end
else
Xlocal = X # I don't need a copy when I don't use threads but suddenly `Xlocal` in the other branch is not local anymore.
end I think the current behavior of |
Even if this behavior doesn't change, this issue was closed despite presenting a still uncompleted action item - that's why I left it open in the first place. I'm assuming that nobody would argue against documenting this behavior explicitly (if this is the behavior we're keeping)? Since that documentation still doesn't exist, I'm going to reopen this. |
@andreasnoack: the comment in your code snippet is truncated, so I'm not sure what the confusion is? |
@kpamnany Sorry. Have updated the comment. The problem is that the use of |
OpenMP deals with this by supporting I believe that once some documentation is in, this will not be very confusing. We can certainly leave the issue open to remind us (me I guess) to write this up. |
We have |
In my applications, it seems as if local variable are more common, e.g. the code that made me post here would require ten if use_threads
fooThreads!(a,b,c,d,e)
else
fooSeq!(a,b,c,d,e)
end with function fooThreads!
@threads for i in 1:nthreads()
...
end
end I've ended up with a similar pattern in other use cases of |
I'd try to not have anything like |
In the final version, it should be fine to control threading with |
I guess this is more or less closed by #33119 (and #30896 is basically a duplicate of this issue), but this was left open because we lack documentation explaining how to avoid this problem. I guess now we should add documentation explaining how to use In fact, it looks like |
I think this issue has nothing to do with threads. The confusion comes from the scoping rules - the assignment to a variable after the cycle makes this variable non-local in the cycle. So, it's about ordering. And from a newbie perspective this is not intuitive. This is also inconsistent with the interactive global scoping rules (for an obvious reason - we can't know the future global scope). I've edited the second part of this comment since I found this motivation for the scoping rule. This is sensible indeed. BTW, that comment explains the rule more clearly than docs. I think docs should emphasize on this more since Julia is different from many other languages in this regard. |
It would make sense to emit a compiler warning if a variable leaves a scope only because of how it is used later in the same function. That could be extended by a simple mechanism to declare local variables further up in a function. Stefan Karpinski's argument (semantics should be independent of order) implies that a variables' scope begins before it is used the first time. This is counter-intuitive inside a function where statements are always executed in order, and it catches people by surprise. |
I ran into this very strange bug while exploring the new multithreading stuff. The smallest reproducible case I can come up with:
Note that the only difference between the above two functions is
w2 = 1
andw = 1
, and that neither variable is actually used after being assigned.Running the above in the REPL:
It appears that some iterations of the multithreaded loop in
wrong(v)
aren't always being executed, or possibly that the binding of the loop variablew
tows[t]
is broken. Either way, one can induce the correct behavior by making sure the post-loop binding to1
doesn't match the loop variable name.I thought maybe that this was a escaping/scoping bug in
_threadsfor
, but I didn't see anything that jumped out at me.@mlubin
The text was updated successfully, but these errors were encountered: