I was reading the documentation on per-field atomics, and it seems that @atomic
can be put in front of any type of field (unlike Base.Threads.Atomic
) as long as the struct
is mutable. I also tested it with the following code which ran just fine:
julia> mutable struct TT{T}
@atomic a::T
end
julia> t1 = TT(fill(1))
TT{Array{Int64, 0}}(fill(1))
julia> @atomic t1.a = fill(2)
0-dimensional Array{Int64, 0}:
2
Does this mean that @atomic
recursively applies the atomic version of setproperty!
to the fields of a composite-type field object? Or does it fall back to thread-unsafe version when the field is not of a primitive type?
Thanks!
Does this mean that @atomic recursively applies the atomic version of setproperty! to the fields of a composite-type field object?
Not quite there is nothing recursive about the use of @atomic
here.
t1.a
contains a reference to an array. t1.a = b
replaces that reference with another one. Only the update of the reference needs to be atomic.
Now if you have:
t1 = TT((1,2))
@atomic t1.a = (2, 1)
Tuples are stored inline
julia> sizeof(TT((1,2)))
32
and so the question is. How does Julia make sure that this
is done atomically?
julia> f(t1, b) = @atomic t1.a = b
f (generic function with 1 method)
julia> @code_llvm f(TT((1,2)), (2, 3))
; Function Signature: f(Main.TT{Tuple{Int64, Int64}}, Tuple{Int64, Int64})
; @ REPL[4]:1 within `f`
define void @julia_f_1185(ptr noalias nocapture noundef nonnull sret([2 x i64]) align 8 dereferenceable(16) %sret_return, ptr noundef nonnull align 8 dereferenceable(32) %"t1::TT", ptr nocapture noundef nonnull readonly align 8 dereferenceable(16) %"b::Tuple") #0 {
top:
; ┌ @ Base.jl:75 within `setproperty!`
%.a_ptr = getelementptr inbounds i8, ptr %"t1::TT", i64 16
call void @jl_lock_value(ptr nonnull %"t1::TT")
call void @llvm.memcpy.p0.p0.i64(ptr noundef nonnull align 8 dereferenceable(16) %.a_ptr, ptr noundef nonnull align 8 dereferenceable(16) %"b::Tuple", i64 16, i1 false)
call void @jl_unlock_value(ptr nonnull %"t1::TT")
call void @llvm.memcpy.p0.p0.i64(ptr noundef nonnull align 8 dereferenceable(16) %sret_return, ptr noundef nonnull align 8 dereferenceable(16) %"b::Tuple", i64 16, i1 false)
ret void
; └
}
So if we can’t use an atomic instruction, because your CPU may not support the use of them, Julia falls back to using locks transparently.
Cool! Thanks for the explanation!
So I can safely assume that whatever field is annotated by @atomic
should be thread-safe (regardless the underlying implementation is an optimized atomic operation or a basic lock mechanism), as long as the evaluation of the mutation expression also annotated by @atomic
does not prompt an error, right?