1

I want to update two atomic variables under an if condition and the if condition uses one of the atomic variable. I am not sure if both these atomic variables will be updated together or not.

I have a multithreaded code below. In "if(local > a1)" a1 is an atomic variable so will reading it in if condition be atomic across threads, In other words if thread t1 is at the if condition, will thread t2 wait for a1 to be updated by thread t1? Is it possible that a2 is updated by one thread and a1 is updated by another thread?

// constructing atomics
#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread
#include <vector>         // std::vector

std::atomic<int> a1{0};
std::atomic<int> a2{0};

void count1m (int id) {
         double local = id;
         double local2 = id*3;
         *if(local > a1) {*      // a1 is an atomic variable so will reading it in if condition be atomic across threads or not?
                a1 = local;
                a2 = local2;
        }
 };

int main ()
{
        std::vector<std::thread> threads;
        std::cout << "spawning 20 threads that count to 1 million...\n";
        for (int i=20; i>=0; --i) {
           threads.push_back(std::thread(count1m,i));
        }
        
        for (auto& th : threads) th.join();
        cout << "a1 = " << a1 << endl;                                                                                                  
}
Pirate
  • 29
  • 3
  • 2
    No, that's not what "atomic" means. You want a mutex (or spinlock or whatever) – Useless Jan 13 '23 at 21:32
  • Either that or you have to pack both variables into a single atomic (`atomic` for example, and rewrite your code to do a non-blocking update like CAS) – Useless Jan 13 '23 at 21:35
  • How can I pack two doubles? – Pirate Jan 13 '23 at 21:52
  • 1
    You can't really, unless you know you have native 128-bit atomics. It's really going to be much easier to use a mutex unless (or until) you know beyond a doubt that lock-free atomics are absolutely essential – Useless Jan 13 '23 at 22:01
  • 1
    don't use `*` in code for bold, it doesnt make it bold – 463035818_is_not_an_ai Jan 13 '23 at 22:02
  • 1
    atomic means that you wont see a variable partially updated, it doesn't provide any waiting or locking semantics. Atomics can be used to make locking and waiting semantics – pm100 Jan 13 '23 at 22:10

2 Answers2

3

I am not sure if both these atomic variables will be updated together or not.

Not.

Atomic means indivisible, in that writes to an atomic can't be read half-done, in an intermediate or incomplete state.

However, updates to one atomic aren't somehow batched with updates to another atomic. How could the compiler tell which updates were supposed to be batched like this?

If you have two atomic variables, you have two independent objects neither of which can individually be observed to have a part-written state. You can still read them both and see a state where another thread has updated one but not the other, even if the stores are adjacent in the code.

Possibilities are:

  1. Just use a mutex.

    You ruled this out in a comment, but I'm going to mention it for completeness and because it's by far the easiest way.

  2. Pack both objects into a single atomic.

    Note that a 128-bit object (large enough for two binary64 doubles) may have to use a mutex or similar synchronization primitive internally, if your platform doesn't have native 128-bit atomics. You can check with std::atomic<DoublePair>::is_lock_free() to find out (for a suitable struct DoublePair containing a pair of doubles).

    Whether a non-lock-free atomic is acceptable under your mutex prohibition I cannot guess.

  3. Concoct an elaborate lock-free synchronization protocol, such as:

    • storing the index into a circular array of DoublePair objects and atomically updating that (there are various schemes for this with multiple producers, but single producer is definitely simpler - and don't forget A-B-A protection)

    • using a raw futex, or a semaphore, or some other technically-not-a-mutex synchronization primitive that already exists

    • using atomics to write a spinlock (again not technically a mutex, but again I can't guess whether it's actually suitable for you)

The main issue is that you've said you're not allowed to use a mutex, but haven't said why. Does the code have to be lock-free? Wait-free? Does someone just really hate std::mutex but will accept any other synchronization primitive?

Useless
  • 64,155
  • 6
  • 88
  • 132
  • 1
    Note that GCC reports 16-byte objects as non-lock-free on x86-64 even though there's no mutex needed (except on early K8 CPUs without `lock cmpxchg16b`). Mainly because efficient atomic load of the whole thing isn't available until very recently when Intel (and maybe AMD) have documented that the AVX feature bit means 16-byte aligned loads/stores are atomic. Otherwise it has to use `lock cmpxchg16b` for `var.load()`, which makes readers contend with each other. (See [How can I implement ABA counter with CAS?](//stackoverflow.com/q/38984153)). Recent AArch64 can do 16-byte atomics efficiently. – Peter Cordes Jan 14 '23 at 03:41
  • Also note the OP's actual atomic variables are `std::atomic` so 2 of them will fit in 8 bytes, easily atomic on most modern systems. They're using `double` locals for no apparent reason. They said in comments "how can I pack two doubles", so maybe they did intend to use `atomic`, in which case yeah you have a 16-byte pair, unless you can use `float`. – Peter Cordes Jan 14 '23 at 03:43
  • 1
    For infrequent updates of something a bit too large to be atomic itself, a standard technique is a SeqLock. [A readers/writer lock... without having a lock for the readers?](https://stackoverflow.com/q/61237650) / [Implementing 64 bit atomic counter with 32 bit atomics](https://stackoverflow.com/q/54611003) That's non-blocking for readers most of the time, and readers are always purely read-only. Unlike rolling your own spinlock to get mutual exclusion. – Peter Cordes Jan 14 '23 at 03:47
  • 1
    Perhaps OP can clarify what size they actually need? – Useless Jan 14 '23 at 09:51
  • Yup, regardless, if they want two 32-bit objects, that's even better news. BTW, a spinlock is by definition not a lock-free synchronization protocol. The building blocks can be individually lock-free, but the algorithm is a lock so one thread stalling indefinitely can and will stop other threads from making progress. https://en.wikipedia.org/wiki/Non-blocking_algorithm . If someone wants to avoid a mutex, it's not because they want to avoid pulling in that part of the library or avoiding the word mutex in their source code. I think we actually can guess about the suitability of that :P – Peter Cordes Jan 14 '23 at 10:08
  • We can guess, but only OP can clarify – Useless Jan 14 '23 at 11:19
0

There are basically two ways to do this and they are different.

The first way is to create an atomic struct that will be updated at once. Note that with this approach there is a race condition where the comparison between local and aip.a1 might change before aip is updated.

struct IntPair {
    int a1;
    int a2;
};

std::atomic<IntPair> aip = IntPair{0,0};

void count1m (int id) {
         double local = id;
         double local2 = id*3;
         if(local > aip.load().a1) {
                aip = IntPair{int(local),int(local2)};
        }
};

The second approach is to use a mutex to synchronize the entire section, like below. This will guarantee that no race condition occurs and everything is done atomically. We used a std::lock_guard for better safety rather than calling m.lock() and m.unlock() manually.

IntPair ip{0,0};
std::mutex m;
void count1m (int id) {
         double local = id;
         double local2 = id*3;
         std::lock_guard<std::mutex> g(m);
         if(local > ip.a1) {
                ip = IntPair{int(local),int(local2)};
        }
 };
Something Something
  • 3,999
  • 1
  • 6
  • 21