Run with multiple threads:
julia> mutable struct Flag
@atomic x::Bool
end; flag = Flag(true);
julia> function exhibit_tearing(flag)
ref = Ref((0,0,0,0))
reader = Threads.@spawn begin
while @atomic flag.x
(r1, r2, r3, r4) = ref[]
( r1 == r2 == r3 == r4) || throw((r1,r2,r3,r4))
end
end
for i=1:1_000_000_000
@atomic flag.x = true
ref[] = (i,i,i,i)
end
@atomic flag.x = false
fetch(reader)
nothing
end
julia> exhibit_tearing(flag)
ERROR: TaskFailedException
Stacktrace:
[1] #wait#585
@ ./task.jl:363 [inlined]
[2] wait
@ ./task.jl:360 [inlined]
[3] fetch
@ ./task.jl:525 [inlined]
[4] exhibit_tearing(flag::Flag)
@ Main ./REPL[3]:14
[5] top-level scope
@ REPL[4]:1
nested task error: (900314, 900314, 900314, 900315)
Stacktrace:
[1] (::var"#exhibit_tearing##0#exhibit_tearing##1"{Flag, Base.RefValue{NTuple{4, Int64}}})()
@ Main ./REPL[3]:6
The atomic Flag in this example is here to force the compiler to not optimize the loop away. In other words, above is not a case of “the compiler mis-optimized your stuff”, it is an example of “the compiler did exactly what you asked of it, you simply misunderstood your processor manual”.
To see an example of the compiler fucking you up, consider
julia> function foo(r)
while r[] end
nothing
end
julia> @code_llvm foo(Ref(true))
; Function Signature: foo(Base.RefValue{Bool})
; @ REPL[4]:1 within `foo`
define void @julia_foo_2866(ptr noundef nonnull align 1 dereferenceable(1) %"r::RefValue") local_unnamed_addr #0 {
top:
%0 = load i8, ptr %"r::RefValue", align 1
%"r::RefValue.x" = trunc i8 %0 to i1
; @ REPL[4]:2 within `foo`
br i1 %"r::RefValue.x", label %L1, label %L4
L1: ; preds = %L1, %top
br label %L1
L4: ; preds = %top
ret void
Naively you might have assumed that you can end the loop by setting r[]=false in a different thread. Alas, you cannot: There is no synchronization / happens-before inside the loop; so it knows that in later loop iterations, r[] returns either the same value or undef, and it optimizes the check away (i.e. it checks only once and re-uses the result over all loop iterations).
I’m sorry to sound harsh, but this is not a julia issue, it is a basic programming issue.
All the “you need locks on collections” is meant as a shorthand for “julia decided not to add locks to most of the standard collections; if you need locks, add them yourself”. This is important info for experienced people – there are many reasonable design choices that julia could have made!
It is meant for people who know what a race and a lock is, not for beginners in programming.
Teaching programming to beginners is not an easy thing. I don’t have anything good to recommend you in terms of “introduction to programming”. But the julia docs won’t immediately answer that for you, nor will any cool julia implementation details, nor any single hard and fast rule.
The real rule is complex, and only seems simple for people with more background (in other words, it’s very simple, and only seems complex to beginners, just like “how do I solve systems of linear equations / Gauss algorithm” in high school). You can come at it from a practical or theoretical side, but ultimately you will need to learn both. So I’m not sure how helpful the following is to you, but I honestly can’t do better.
The basic model to follow: Stuff happens in the order you expect. Stuff inside a thread / task happens after the thread/task has been scheduled (via e.g. Threads.@spawn), and happens before the task ends. The return of fetch / wait on a task happens after the task ends.
If you have two accesses, at least one of them a write, on the same thing and neither happens before the other, then you have a race.
r=Ref(1) #A
t=Threads.@spawn begin #B
r[] #C
end
r[] = 2 #D
fetch(t) #E
In this case A < B < D < E and B < C < E, but C and D have no happens-before ordering and happen “concurrently”. And at least one of C, D is a write. Hence, race.
You need to do something to address this race. Ideal is to architect your code such that races don’t happen. For example, put line D before B or after E, depending on whether you want to read 1 or 2.
If that doesn’t work, atomics. Alternatively, locks (locks are atomics in a trenchcoat, but are easier to understand for some people).