Search code examples
c++multithreadinglockingatomic

Can you atomically write two uint64_t's?


I have a problem in which I need to be able to atomically update TWO uint64_t's simultaneously. It's easy enough to write each one of them atomically (e.g. have two std::atomic<uint64_t>'s), but that would still lead to a situation where one is updated but the other is not. It's also easily possible with a lock and a mutex.

But I want to write both atomically, without any kind of lock, so that I can still have member variables that are of type uint64_t so that there's no locking on read. This is because my use-case involves reading them lots and lots and lots of times, but writing them exceedingly rarely (~reading 1x/ms, writing 1x/5 min). Is it possible? If so, how?


Solution

  • For std::atomic the standard says that (emphasis mine)

    The primary std::atomic template may be instantiated with any TriviallyCopyable type T:

    struct Counters { int a; int b; }; // user-defined trivially-copyable type
    std::atomic<Counters> cnt;         // specialization for the user-defined type
    

    So you can just create a struct of 2 uint64_t like this

    struct atomic128 {
        uint64_t a1, a2;
    };
    

    which is trivially copyable (it's easy to confirm with std::is_trivially_copyable), and then use std::atomic<atomic128>. You'll get an error if std::atomic<type> is not trivially copyable

    That way the compiler will automatically use lock-free updating mechanism if it's available. No need to do anything special, just check that with either of the below if necessary

    All atomic types except for std::atomic_flag may be implemented using mutexes or other locking operations, rather than using the lock-free atomic CPU instructions. Atomic types are also allowed to be sometimes lock-free: for example, if only some subarchitectures support lock-free atomic access for a given type (such as the CMPXCHG16B instruction on x86-64), whether atomics are lock-free may not be known until runtime.

    std::atomic_is_lock_free and std::atomic::is_lock_free

    Here's a demo on Compiler Explorer. As you can see a lock cmpxchg16b is emitted, although GCC 7 and up will just call __atomic_store_16 which internally use cmpxchg16b if it's available

    On some platforms long double is a 128-bit type or is padded to 128 bits, therefore std::atomic<long double> may be another solution, but of course you need to check its size and whether it's lock-free or not first

    Another alternative is Boost.Atomic. It also has the macros BOOST_ATOMIC_INT128_LOCK_FREE and BOOST_ATOMIC_LONG_DOUBLE_LOCK_FREE to check

    On some CPUs 128-bit SSE operations are also atomic, unfortunately there's no way to check whether you can use that or not

    See also: