Home > Software design >  Updating two atomic variables under a condition in C
Updating two atomic variables under a condition in C

Time:01-14

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;                                                                                                  
}

CodePudding user response:

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?

CodePudding user response:

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)};
        }
 };
  • Related