`Threads.atomic_cas!`, `Threads.SpinLock`, and `@atomic` documentation

Reading the Julia manual, I see that there is a “mild” deprecation for a series of atomic-related functions, including Threads.atomic_cas!. I understand this is in favor of the @atomic macro, which is apparently more granular. However, at least from a user perspective, this opens a number of issues:

  • All @atomic operations I have seen do not seem to reproduce the behavior of Threads.atomic_cas!, so I am left with the doubt if there is way to replace this function;

  • The order symbol in the @atomic macro is very poorly documented. The manual refers to the Julia Atomics Manifesto, which however does not describe the order, but points to yet another document which however is for Rust. From a user perspective this is really confusing (especially if one is not fluent in Rust…).

  • I wanted to use Threads.atomic_cas! to implement a sort of soft lock. Essentially, concurrent threads would check if a shared resource (a block of memory) is free. If it is, they would use it (with a lock); if not, they would perform a slower calculation re-allocating locally the same resource. To check how this is implemented in the Julia code, I browsed the trylock(::Threads.SpinLock) method, and saw this:

    function trylock(l::SpinLock)
        if l.owned == 0
            GC.disable_finalizers()
            p = @atomicswap :acquire l.owned = 1
            if p == 0
                return true
            end
            GC.enable_finalizers()
        end
        return false
    end
    

    I am not sure the schema used here is totally equivalent to Threads.atomic_cas!, since in principle one could have a concurrent thread acquiring the lock just after the if l.owned == 0 line (and before the p = @atomicswap one). In other words: is the trylock function really doing what it promises or should one rather use the Threads.atomic_cas! function?

Thank you

This is mostly equivalent to whatever scheme you would implement with Threads.atomic_cas!. Julia spinlocks also carry the following properties:

  1. If any lock is held, then finalizers are disabled.
  2. If no lock is held, then finalizers are enabled again.
  3. You are not permitted to acquire a lock and then release it on a different task.

In order to get the finalizer thingy working, trylock has to disable finalizers before acquiring the lock; and, if it has disabled finalizers and then fails to acquire the lock, it has to enable them again.

Since disabling and enabling finalizers is not free, trylock uses double-checked locking.

In other words: Yes, it can happen that someone else snatches the lock after the if l.owned == 0 and before p = @atomicswap :acquire l.owned = 1. In that case, p == 1 and we will re-enable finalizers and return false, i.e. fail to acquire the lock. The worst case for this race condition is that we have wasted some cycles enabling and disabling finalizers.

Whether to use Base spinlock depends on whether you care about suppressing finalizers inside critical sections, and whether you might migrate the held lock between tasks.

If you don’t want to use SpinLock, the successor to Threads.atomic_cas! is @atomicreplace if you want to use sugary syntax macros, or Base.replacefield! / Base.replaceproperty! if you prefer non-macro “real” APIs.