44

The naive boolean negation

std::atomic_bool b;
b = !b;

does not seem to be atomic. I suspect this is because operator! triggers a cast to plain bool. How would one atomically perform the equivalent negation? The following code illustrates that the naive negation isn't atomic:

#include <thread>
#include <vector>
#include <atomic>
#include <iostream>

typedef std::atomic_bool Bool;

void flipAHundredThousandTimes(Bool& foo) {
  for (size_t i = 0; i < 100000; ++i) {
    foo = !foo;
  }
}

// Launch nThreads std::threads. Each thread calls flipAHundredThousandTimes 
// on the same boolean
void launchThreads(Bool& foo, size_t nThreads) {

  std::vector<std::thread> threads;
  for (size_t i = 0; i < nThreads; ++i) {
    threads.emplace_back(flipAHundredThousandTimes, std::ref(foo));
  }

  for (auto& thread : threads) thread.join();

}

int main() {

  std::cout << std::boolalpha;
  Bool foo{true};

  // launch and join 10 threads, 20 times.
  for (int i = 0; i < 20; ++i) {
    launchThreads(foo, 10);
    std::cout << "Result (should be true): " << foo << "\n";
  }

}

The code launches 10 threads, each of which flips the atomic_bool a larrge, even, number of times (100000), and prints out the boolean. This is repeated 20 times.

EDIT: For those who want to run this code, I am using a GCC 4.7 snapshot on ubuntu 11.10 with two cores. The compilation options are:

-std=c++0x -Wall -pedantic-errors -pthread
juanchopanza
  • 223,364
  • 34
  • 402
  • 480
  • Looking at the specs, atomic types like `std::atomic_bool` and `std::atomic` don't have boolean operators like `operator!`. So `!b` does indeed involve the (implicit) conversion operator of atomic types. I'm not making this an answer as I'm unsure on how to provide the functionality you need. – Luc Danton Mar 21 '12 at 14:15
  • @LucDanton Right, and I couldn't find anything about a specialised `operator!` for atomics, so I'm sure the conversion happens, and it is most likely the cause of the race. – juanchopanza Mar 21 '12 at 14:19
  • 1
    This is what I am getting when I run your program : `terminate called after throwing an instance of 'std::system_error'` – BЈовић Mar 21 '12 at 14:21
  • @VJovic I am using a GCC 4.7 snapshot, and linking to libpthread. – juanchopanza Mar 21 '12 at 14:24
  • @VJovic I'm assuming you're using GCC on a Posix platform and that it is configured such that you need to pass `-pthread` when compiling the program. – Luc Danton Mar 21 '12 at 14:25
  • @LucDanton Ok, I forgot -pthread – BЈовић Mar 21 '12 at 14:38

1 Answers1

36

b = !b is not atomic because in the C++ source you have an atomic pure read of b (equivalent to b.load(), and then a separately-atomic assignment to b (equivalent to b.store()).

Nothing makes the entire combination into an atomic RMW operation in the C++ abstract machine, and there's no syntax for composing arbitrary operations into atomic RMW operations (other than putting it into a CAS retry loop).


There are two options to use:

  1. Instead of atomic<bool>, use an integral type (e.g. atomic<int> or atomic<unsigned char>) which can be 0 or 1, and xor it with 1:

    std::atomic<int> flag(0);
    
    flag ^= 1;        //equivalent to flag.fetch_xor(1);
    

    Unfortunately, fetch_xor is not provided on atomic<bool>, only on integral types.

  2. Perform a compare/exchange operation in a loop, until it succeeds:

    std::atomic<bool> flag(false);
    
    bool oldValue = flag.load();
    while (!flag.compare_exchange_weak(oldValue, !oldValue)) {}
    

    Unfortunately compilers for x86 won't typically optimize this loop into
    lock xor byte [flag], 1 in the asm; you'll get an actual cmpxchg retry loop. In practice cmpxchg retry loops are fine with low contention. In the worst case this is not wait-free, but is lock-free because at least one thread will make progress every time they all retry. (In practice it's more complicated with hardware arbitration for which core even gets access to the cache line to make an attempt.)

    If high contention is possible, prefer the integer version that lets you use an atomic xor.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
interjay
  • 107,303
  • 21
  • 270
  • 254
  • The second one seems to cause livelock, at least in theory. – zch Mar 21 '12 at 14:42
  • 6
    @zch: I don't think it does. It will only need to loop again if another thread has successfully modified the variable. So some work is always done on at least one thread - in other words, this is lock-free but not wait-free. – interjay Mar 21 '12 at 14:52
  • Option 2 works nicely with my example. Is this the standard pattern for update operations on atomic types? – juanchopanza Mar 21 '12 at 17:25
  • @juanchopanza: It's the standard practice that I've seen used for operations on a single variable that can't be performed atomically. The nice thing is that it can perform any sort of calculation instead of `!oldValue` here. When dealing with atomic pointers you may need to worry about the [ABA problem](http://en.wikipedia.org/wiki/ABA_problem) as well, but that's not an issue here. – interjay Mar 21 '12 at 17:42
  • The standard is fairly confusing about this. It does mention that in addition to the specializations on integral types, there is `atomic` and `atomic_bool`, however `bool` is also specified to be an integral type (3.9.1/7). So it would appear that `fetch_xor` should be defined for it. – Potatoswatter Mar 22 '12 at 01:06
  • 1
    @Potatoswatter it is confusing, but I think §29.5 notes 4 to 8, particularly note 4, separate the atomic specializaton from the rest of integers with full specializations. The requirements on atomic_bool are only slightly different to those of atomic. – juanchopanza Mar 22 '12 at 08:09
  • Option 1 suffers from a homunculus fallacy: If boolean negation requires a read and a write - semantically - than so does an integer XOR. You can't determine the result without reading the previous value. – einpoklum Mar 28 '15 at 19:07
  • 5
    @einpoklum There is no fallacy. `x ^= 1` is a single atomic operation while `x = !x` (and also `x = x ^ 1`) is two atomic operations. – interjay Mar 28 '15 at 21:11
  • x ^= 1 is not a single atomic operation. It could be made one by using compare_exchange under the hood, but it has not been defined that way. x ^= 1 may look like a single operation, but if 'x' is a variable in memory then there is a read and a write and, absent special effort, a read-modify-write sequence is not atomic. – Bruce Dawson Sep 09 '15 at 16:15
  • 2
    @BruceDawson The whole point of `std::atomic` variables is that operations on them, including `^=`, are defined to be atomic. – interjay Sep 09 '15 at 16:31
  • @interjay Citation needed. The references I have looked at say that atomic variables do atomic reads and atomic writes. I have seen nothing about atomic read-modify-write. That's a pity, because that would be helpful, but also much more expensive, and tricky. Most recently I looked at: http://en.cppreference.com/w/cpp/atomic/atomic – Bruce Dawson Sep 11 '15 at 01:41
  • @BruceDawson That reference does say that `^=` and all the other operations are atomic, so I don't know why you think they aren't. Besides, what would be the point of providing them if they weren't atomic? If that's not enough, see the C++ standard, 29.6.5/27-32. – interjay Sep 11 '15 at 09:30
  • Fascinating. That doc actually says ^= "adds, subtracts, or performs bitwise AND, OR, XOR with the atomic value" which doesn't say it's atomic. I think that doc could be more explicit on this vital topic. But having read more carefully I believe you are correct - apologies. What would the point be if read-modify-write wasn't atomic? A few things: - Without std::atomic there is no portable way to guarantee atomic reads or writes - Without std::atomic there is no portable control of reordering These two mean no portable lock-free code. Portable read-modify-write clearly adds additional value. – Bruce Dawson Sep 11 '15 at 20:23
  • 2
    @BruceDawson actually it's much clearer when you follow the link of these operations: https://en.cppreference.com/w/cpp/atomic/atomic/operator_arith2 The operation is read-modify-write operation. Note that nowhere you get a guarantee of a hardware accelerated, lock-free, atomic. So these RMW operations can be implemented with a mutex. – v.oddou Mar 08 '19 at 03:01
  • 1
    @BruceDawson: `operator ^=` for integer atomic types is an overload for `atomic_fetch_xor` which atomically modifies the value in memory (and returns it in case you want the old value). On x86 it compiles to `lock xor`, not to separately-atomic load and store (with an XOR in the middle). Or on non-x86, an LL/SC retry loop. But unfortunately this overload is missing for `atomic`. This is expensive so they wouldn't do this if the language didn't require it, but this is the simple way to use atomic RMW operations in C++. (You need the explicit functions if you want weaker than seq_cst) – Peter Cordes Jul 05 '19 at 01:30