4

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?

phuclv
  • 37,963
  • 15
  • 156
  • 475
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Hmmm.... sounds like you are looking at the wrong type of locking pattern, perhaps. – Andrew Barber Oct 16 '13 at 03:56
  • 3
    Not possible with C++. If you're on an x64 CPU, there is a `CMPXCHG16B` instruction you could use if the two 64-bit values are side-by-side and 16-byte aligned. – Cory Nelson Oct 16 '13 at 03:59
  • 1
    The objects you can access atomically without locks depends on the hardware. If the hardware doesn't support 128 bit atomic objects then you simply can't do it. The atomic template is the way to abstract away this difference, using lock on hardware that requires it and using hardware support where available. – bames53 Oct 16 '13 at 04:06
  • @CoryNelson Suppose I they were side-by-side and aligned, how would I use that instruction? – Barry Oct 16 '13 at 04:08
  • I removed my answer - it applies only when the writes can happen atomically - this was the case on the hardware where I learned that trick. Sorry about that. – Wayne Uroda Oct 16 '13 at 04:37
  • It's not the same question but there might be some insights in this answer http://stackoverflow.com/questions/18177622/how-to-atomically-add-and-fetch-a-128-bit-number-in-c – jcoder Oct 16 '13 at 04:59
  • Can you give more details of the usage pattern? It might make sense for each thread to have their own copy of the two values (which would require no locking and no atomic operations) if you can then combine the values from each of the threads once they're all done. – Adrian McCarthy Mar 11 '19 at 17:17

2 Answers2

4

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:

phuclv
  • 37,963
  • 15
  • 156
  • 475
  • The name is just for example, it's not very important. The OP should decide the one that most reflect his work. Anyway, I've just edited the answer – phuclv Oct 16 '13 at 07:04
  • -1 Instantiating `atomic` on arbitrary types without providing a full specialization for that type is almost never what you want and will certainly cause a surprised look on any programmer's face who has to touch that code. – ComicSansMS Oct 16 '13 at 09:02
  • 1
    Note, however, that atomic updates to an object of type `std::atomic` are not required to be lock-free. – Pete Becker Oct 16 '13 at 13:24
  • @ComicSansMS a simple struct like that is TriviallyCopyable and can be used with `std::atomic`. You can see the same example in `std::atomic` documentation – phuclv Oct 17 '20 at 04:23
2

I don't think it's possible to do directly, but what you can do is use software transactional memory techniques to fake it. In particular, you could use a lock-free ring buffer of uint64_t-pairs. In this configuration, writing into a non-active element of the ring-buffer is safe to do non-atomically, because nobody will read from that element of the ring-buffer until after the "current-value-index" is atomically updated at the end of the writes (which is possible because the index can be an int32_t).

Caveat: this trick only works if you can guarantee that there won't be too many writes to the value in a short period of time (where 'too many' means 'more than the number of slots in the ring-buffer). Also I'd recommend finding an STM library that implements this rather than rolling your own, as lock-free programming is notoriously difficult to get 100% right.

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234