3

I have never used atomic operations, and remain largely ignorant of how to utilize them. And yet, I often come across objects like this when peering into Qt's backend: https://doc.qt.io/qt-6/qatomicinteger.html

This leads me to question if there is a feature here that I have not been properly taking advantage of.

My question therefore is regarding,

  • When should one utilize atomic operations?
  • Is it supposed to improve performance?
  • Are there other considerations besides performance?
  • When would one look at someones code, and chastize them for having not used atomic operations?

Looking back, I am wondering if there are places that I should have been using them, but just went with a mutex instead.

Anon
  • 3,565
  • 3
  • 27
  • 45
  • 1
    *"When would one look at someones code, and chastize them for having not used atomic operations?"* - If you see code you think could be improved or written differently, then just discuss it with them by suggesting a change, explaining the reasoning as needed; there's no reason to make it personal, and remember there may be perfectly good reasons for way the code had originally been written (if there are any potential issues or problems it may simply have been that the person had been completely unaware of those problems or might have just made a simple honest mistake.). – Ben Cottrell Jul 29 '22 at 10:56

2 Answers2

7

Atomic operations are useful for writing safe concurrent/multithreaded code with shared mutable state without having to resort to (expensive) locks or mutexes. For small data types such as integers or pointers, atomics allow us to replace locks like

{
  QMutexLocker locker(&mutex);
  x += 1;
}

with atomics like

atomicX.fetchAndAddAcquire(1);

Or with the C++ standard library, we can replace
{ std::lock_guard<std::mutex> guard(mutex); x += 1; }
with
atomicX.fetch_add(1, std::memory_order_acq_rel).

This has potential performance benefits since CPUs provide dedicated instructions for atomic operations. Also, there are different "memory orderings" with different guarantees about when which value is visible to a different CPU core. Atomics allow us to select the appropriate degree of control here, with more relaxed orderings leading to potentially better performance than using a stricter ordering or a mutex/lock. Memory orders are defined in the C++ standard, for example see the summary on cppreference.com.

Specifically on x86 architectures, you will likely not see a difference between ordinary volatile variables and atomics with relaxed or acquire/release orderings. The CPU architecture already provides strong guarantees for all memory accesses. However, accurate use of memory orderings is quite relevant on ARM architectures. Using too relaxed memory orders (or no atomics at all) could corrupt data.

Specific answers to your questions:

  • When should one utilize atomic operations?

    When all of the following hold:

    • you have multiple threads that share mutable state
    • your modifications to this state only affect single words (integers, pointers, …)
    • you want to avoid locks/mutexes

    Counter-indications:

    • the data is only used by a single thread → use ordinary variables
    • the shared data remains constant → use ordinary variables
    • changes to the shared data affect more than one word at a time → use locks/mutexes
    • you do not want to learn about memory orderings → use atomics with sequentially consistent ordering or locks/mutexes
  • Is it supposed to improve performance?

    Primarily, it is intended to improve correctness.

    But different memory orders allow us to select the most relaxed (and therefore fastest) memory order that still meets our needs. Of course we could always impose a sequentially consistent order (memory_order_seq_cst) but that is generally slowest and will involve CPU-level locks.

  • Are there other considerations besides performance?

    Correctness.

  • When would one look at someones code, and chastize them for having not used atomic operations?

    Not at all.

    • Chastizing people is usually not very didactic – it builds resentment.
    • If someone shares mutable state across threads and doesn't protect accesses via mutexes, locks, or atomics, there are potential race conditions. It could make sense to raise this issue.
    • If someone uses mutexes or locks to protect single-word data changes, switching to atomics could lead to simplifications and performance improvements.
amon
  • 132,749
  • 27
  • 279
  • 375
  • What is auto's type supposed to be? I – Anon Jul 29 '22 at 11:44
  • @Anon The `guard` variable is supposed to represent the held lock on the mutex. Once the guard goes out of scope, its destructor will run and the mutex will be unlocked. The specific type of the guard is not relevant here, so I used `auto` to get C++11 type inference. – amon Jul 29 '22 at 11:47
  • https://doc.qt.io/qt-5/qmutex.html#lock << returns a void. Its kind of throwing me off. Can you just be explicit here? My heuristic when seeing auto inside of code is to assume that the type neccessarily has to be inferred by the compiler. If that is the case here, I would like to know. Otherwise its a bit of a red herring for me. – Anon Jul 29 '22 at 11:53
  • `/home/anon/Programming/QtConsoleDesigner/QtSandbox/src/sneed.cpp:6:14: error: 'void guard’ has incomplete type` Incidentally, I had to double check whether it was possible to use auto for a void function. – Anon Jul 29 '22 at 12:00
  • @Anon please understand that code snippet as pseudocode, not as a reference to any particular Mutex implementation. – amon Jul 29 '22 at 12:32
  • 2
    An other advantage of atomic variables over mutexes is to reduce programmer error. When using mutexes, sometimes you can forget to lock (or unlock) the mutex when accessing the variable. With the atomic variables you don't have to care about that. – f222 Jul 29 '22 at 12:44
  • 1
    @amon: well, replacing this "pseudo code" by a piece which follows a little bit more the standard mutex syntax would less astonish readers, without making the answer much longer . – Doc Brown Jul 29 '22 at 19:01
  • @DocBrown Eh, but you might have a point. I edited the code snippet to use the Qt API, and a more generally useful example with the standard library types. – amon Jul 29 '22 at 20:29
-3

If you ask the question, then you shouldn't. You should check if you have multi-threaded code, where multiple threads can read or write the same variable (with at least one writer), and then read up on the rules of your language, possible of your processor, for this situation, whether this will cause you problems (it probably will cause you hard to find problems), and them you solve these problems by using mutexes, atomic access, serial queues, or best by asking someone for help who knows how to do this.

But the principle is this: If all threads using a variable always use atomic operations, and some thread modifies the variable using an atomic operation, then any other thread either sees that the modification hasn't started yet, or that it has finished. It never sees the variable in a state in between.

gnasher729
  • 42,090
  • 4
  • 59
  • 119