Is it possibly to do atomic update on an array element?

The manual seems to read “primitive type values only”.
Also, just above that section is “API is deprecated”.

Does anyone has an insight into this?

1 Like

Is this a good way:

julia> avec = [Threads.Atomic{Int}(0) for _ in 1:5]
5-element Vector{Base.Threads.Atomic{Int64}}:
 Base.Threads.Atomic{Int64}(0)
 Base.Threads.Atomic{Int64}(0)
 Base.Threads.Atomic{Int64}(0)
 Base.Threads.Atomic{Int64}(0)
 Base.Threads.Atomic{Int64}(0)

julia> avec[4][] = 10
10

julia> avec
5-element Vector{Base.Threads.Atomic{Int64}}:
 Base.Threads.Atomic{Int64}(0)
 Base.Threads.Atomic{Int64}(0)
 Base.Threads.Atomic{Int64}(0)
 Base.Threads.Atomic{Int64}(10)
 Base.Threads.Atomic{Int64}(0)

and using Atomic type according to the ways detailed in the documentation: Multi-Threading · The Julia Language

That is one way, but it’s generally not going to be fast and it only works for a limited set of types. It’s also what’s eventually to be replaced by the new @atomic macros — but indeed I’m not sure how that’d work with an array.

Regardless, t’s often much better to use other constructs (like coarser-grained locks or channels) or more carefully segment your accesses and writes so you don’t have to deal with the overhead of synchronizing the state between cores.

1 Like

An unclean way would be to add a lock to the special type in the array. Something like:

mutable struct NewAtomicMutable
    lck::ReentrantLock
    a::Int
    b::Int
end

cvec = [NewAtomicMutable(Base.ReentrantLock(), 0, 0) for _ in 1:5]

for i in 1:10
    idx = rand(eachindex(cvec))
    lock(cvec[idx].lck)
    try
        cvec[idx].a += 1
        cvec[idx].b += 2
    finally
        unlock(cvec[idx].lck)
    end
end

The last loop atomically modifies the data fields of the mutable struct.
This is not very clean, and can be abstracted to a new Array type. Perhaps someone knows about an existing implementation (sounds like a construct which would in occasional use).

Actually, for strictly update operations, a SpinLock might be more performant:

mutable struct NewAtomicSpin
    lck::Base.Threads.SpinLock
    a::Int
    b::Int
end

cvec = [NewAtomicSpin(Base.Threads.SpinLock(), 0, 0) for _ in 1:5]

(can be used with the same update loop)

When will the new macro land, do you think?

Btw: I’ve seen papers that claim atomics can work well in C++ (2011) programs.
It might be worthwhile to check that out.

Note that as of Julia 1.11, there is an actually right way of doing this which is AtomicMemory.

5 Likes

The new APIs are already here, since v1.7: Multi-Threading · The Julia Language. That’s why the stuff below it is marked as deprecated.

I believe the new way to work with array elements isn’t quite user-friendly and documented yet — it’s the “primitive” and unsafe pointer operations towards the end of that section (edit: see Oscars post above). I’ve not tried the new APIs yet.

Before 1.11 there is Atomix.jl

I wonder how expensive (memory-wise) that would be?

Correctness has a price, and if the integrity of the data-structure making array elements is at stake, it’s worth paying it. This connects to a lot of discussion of row-based locking vs table locks in DBs and key-value stores.

This is for very large arrays, though. Easily over a billion elements.

In that case, you might want to partition the array into chunks and have locks over those chunks. Having a lock per element will be really expensive.

2 Likes

There is a paper (On Memory Traffic and Optimisations for Low-order Finite Element Assembly Algorithms on Multi-core CPUs | ACM Transactions on Mathematical Software) which claims good scalability with OpenMP using atomic access to array elements. Intriguing…

If your eltype has support for native atomic operations then it will be pretty free (locks are at least a word, but atomic fields are free for supported types). It sounds like 1.11 AtomicMemory or Atomixs.jl is what you want then.

1 Like

FWIW, there is a small wrinkle with Vector before the big Memory change – you can memory corrupt if a different thread reallocs the thing via resizing.

Almost surely not a problem for your intended use, but probably the reason why this wasn’t always a part of the official API (this was unfixable with the old layout).

1 Like