Using atomic operations in Julia 1.6

I’d like to implement the following pattern:

mutable struct A
     @atomic x::Int
end
a = A(3)
@atomic a.x = 4 # in a multithreaded environment

However, I don’t want to drop compatibility with Julia 1.6 and it seems the @atomic macro was introduced in Julia 1.7. Is the above syntactic sugar / equivalent to something that can be expressed in Julia 1.6? Or is this ability fundamentally reliant on changes introduced in Julia 1.7?

The capability for atomic field access was introduced with this PR and is linked to the runtime - in particular the language level guarantees. Features are not backported, so there’s no equivalent providing the same guarantees in 1.6.

1 Like

How does my example differ from changing the type of x to Threads.Atomic{Int} and using the square brackets x[] when assigning to x? (edited)

There are a lot of libraries doing fancy threaded stuff that support Julia 1.6 (the LTS), so I feel like I must be missing something as my use case is quite basic…

I guess my question is: what is the difference between @atomic and friends v.s. using Threads.Atomic and atomic_add! etc? Does one generalize the other? Or do they just do different things?

@atomic supports more types, not just some bitintegers (most importantly, pointers/things that are stored as a pointer internally). Moreover, things like x[] += 1 (that someone can write naively) can introduce subtle race conditions that @atomic prevents by requiring all accesses to go through that macro. As another benefit, @atomic supports memory ordering of accesses, which is more lightweight (and requires compiler support) than the heavy approach that Atomic{T} requires.

Lots of threaded code can use locks and explicit synchronization points instead of atomic operations, or doesn’t need locks at all if no state is shared between threads. Atomic operations are no panacea either - sometimes you just need a heavier tool.

I don’t know what your background is, but if you search for “memory ordering” you are sure to find lots of ressources about the topic. This may be of particular interest.

2 Likes

Why do you want to keep compatibility with 1.6? Since it’s not easily possible, maybe you can make a macro that would translate to a no-op on 1.6, and to @atomic on newer?

It would be dangerous if you run on 1.6 with threads (or your users), but if you can be sure they don’t I think it will be safe.

That’s arguably a bad idea, since the semantics of memory ordering are lost, which WILL introduce subtle bugs or even eliminate some code entirely, because the compiler can’t see that another thread is accessing the variable.

This is not easily backportable in any way that preserves the guarantees you want of it.

1 Like

Thanks for your answers! I understand now that @atomic has some nice semantics for ensuring safety. However, assuming that I do use only safe operations for Threads.Atonic (e.g. atomic add rather than +=), is this going to be essentially semantically equivalent to the analogous @atomic approach? My very rough understanding of the memory ordering stuff is that it should affect performance only rather than correctness, assuming the program is race-free? (You said Threads.Atomic was a more heavyweight approach – does that cotrespond to using the strictest memory ordering guarantee?)

Right, why I mentioned it “would be dangerous”. But not without threading(?). So I’m not suggesting a macro for Julia Base, since it would be unsafe. Just thinking if people could do it. Better is to abandon Julia 1.6 LTS (e.g. if you need threading), and maybe 1.7 (or later) will be chosen as a new LTS, soon (I’ve not heard it confirmed).

Depending on what you’re doing, this might be fine, but it really depends. The main differences w.r.t ordering are that you can’t control what ordering is used for Threads.Atomic{T}; for example, atomic_add! always uses acquire-release ordering. If this is what you need, and your element type is supported, then it’ll be mostly equivalent to using @atomic :acquire_release x.a += y (and thus performance should be similar).

I recommend checking out base/atomics.jl in the Julia repository and cross-referencing with what’s in LLVM Language Reference Manual — LLVM 16.0.0git documentation to determine if the available calls suit your needs.

3 Likes

Heavyweight in terms of API - you’re forced to use the existing functions and can’t change their memory ordering, as well as only being able to use a limited number of types.

None of that has been any topic of discussion, and LTS is not going to change so soon after it just got changed to 1.6 half a year ago.

1 Like