4

I've read https://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange

Atomically compares the object representation (until C++20)value representation (since C++20) of *this with that of expected, and if those are bitwise-equal, replaces the former with desired (performs read-modify-write operation). Otherwise, loads the actual value stored in *this into expected (performs load operation).

So as I understand the code like

bool expected=true;
extern atomic<bool> b = false; 
void foo ()
{
//
    while(!b.compare_exchange_weak(expected, false));
//
}

after the loop will run once(ignoring spurious failure) it will fail, and will write to expected false, so on the second iteration compare_exchange_weak will return success, though b hasn't been changes to true. But what's the point of all this? I though I could use this as a lock for synchronization, waiting for other thread to change b, but now I can't think of a usage of this.

Example from the cppreference also shows that after two calls compare_exchange_strong will succeed.

#include <atomic>
#include <iostream>
 
std::atomic<int>  ai;
 
int  tst_val= 4;
int  new_val= 5;
bool exchanged= false;
 
void valsout()
{
    std::cout << "ai= " << ai
          << "  tst_val= " << tst_val
          << "  new_val= " << new_val
          << "  exchanged= " << std::boolalpha << exchanged
          << "\n";
}
 
int main()
{
    ai= 3;
    valsout();
 
    // tst_val != ai   ==>  tst_val is modified
    exchanged= ai.compare_exchange_strong( tst_val, new_val );
    valsout();
 
    // tst_val == ai   ==>  ai is modified
    exchanged= ai.compare_exchange_strong( tst_val, new_val );
    valsout();
}

Result:

ai= 3  tst_val= 4  new_val= 5  exchanged= false
ai= 3  tst_val= 3  new_val= 5  exchanged= false
ai= 5  tst_val= 3  new_val= 5  exchanged= true
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Bruice
  • 543
  • 3
  • 11
  • Does this answer your question? [Understanding std::atomic::compare_exchange_weak() in C++11](https://stackoverflow.com/questions/25199838/understanding-stdatomiccompare-exchange-weak-in-c11) – rustyx Mar 05 '21 at 14:08
  • `compare_exchange` gives you transaction style of synchronization. If transaction fails recalculate result and try submit it again. – Marek R Mar 05 '21 at 14:16
  • @rustyx no, there is no explanation where and for what it std::atomic::compare_exchange should be used – Bruice Mar 05 '21 at 14:17
  • _"I really don't get [it] ... what's the point of all this?"_ may be too broad a question to get the particular answer you are looking for. Are you asking for a real-world example? – Drew Dormann Mar 05 '21 at 14:25
  • @DrewDormann yes, I don't really understand for now what's the usage and point of his – Bruice Mar 05 '21 at 14:26
  • The non-matched value is returned to you in case you're interested to know, but apparently you are not. So just ignore it. `while(!b.compare_exchange_weak(expected = true, false));`. – rustyx Mar 05 '21 at 14:27
  • That's not a particularly helpful example -- it's pointless when there's only a single thread modifying the value. When you have multiple threads you can use this to avoid data races. – Pete Becker Mar 05 '21 at 14:33
  • Best [explanation of difference between those two I know](https://youtu.be/ZQFzMfHIxng?t=1983). – Marek R Aug 10 '22 at 16:13

2 Answers2

5

std::atomic::compare_exchange_weak performs this English-language task, in a thread-aware way:

Because the variable holds expected, it should now hold desired.

As a trivial task, imagine that the value of your std::atomic<int> x should be squared. But other threads may be modifying it, so you can't simply read the value, square it and write the new value back. The value may have changed after you read it!

Here is a thread-safe way to perform this task. You are guaranteed that the stored value will be replaced with its square.

int expected = x;
while( !x.compare_exchange_weak(expected, expected*expected) );

This code will atomically replace expected with its square unless the value has changed.

If the value has changed, expected is now updated with the new value and the code tries again.

Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
3

I will give an example where I did used that, since it is very simple one.

I had atomic which describes available size of something. There was a danger of integer overflow, so I had do checking first before subtracting a value.

Not exact copy paste of my production code:

class LockFreeCuncuretSizeLimit {
public:
    explicit LockFreeCuncuretSizeLimit(size_t available) : mSize{available}
    {}

    bool tryAcuqire(size_t s) {
        size_t lastSize = mSize;
        size_t newSize;
        do
        {
            if (lastSize >= s)
            {
                newSize = lastSize - s;
            }
            else
            {
                return false;
            }

        }
        while (!mSize.compare_exchange_weak(lastSize, newSize));
        return true;
    }

    void release(size_t s) {
        mSize += s; // here I do not have worry about integer overflow
    }
private:
    std::atomic<size_t> mSize;
};

Now try image do that without compare_exchange_strong and not having a race condition.

There is a change that condition is meet, but when subtraction is done on atomic some other thread already subtracted a value so when I do actual subtraction it is possible to overflow integer. So this cant be done without compare_exchange_strong.

Now difference between compare_exchange_strong and compare_exchange_weak is hard to explain. Even Herb Sutter on some cppcon talk give up explaining that and provided simple rule: "if you need loop use compare_exchange_weak otherwise use compare_exchange_strong".

Marek R
  • 32,568
  • 6
  • 55
  • 140
  • So imagine other thread have changed mSize after in current thread lastSize was initialized. When we reach while (!mSize.compare_exchange_strong(lastSize, newSize)); it will fail, lastSize will be updated with the new, changed from other thread value of mSize and on the next iteration will assign mSize with newSize. I still don't understand the meaning of this :( – Bruice Mar 05 '21 at 14:43
  • 1
    This is transaction synchronization. Like in databases, but on small objects. `compare_exchange_strong` is attempt to commit transaction. If commit fails since input value was changed this code loops again, recalculates new result and then tries to commit updated value. – Marek R Mar 05 '21 at 14:46
  • 1
    Try imagine two threads running in parallel same lines of code. All values will be same until `mSize.compare_exchange_strong` is reached. In this place those two threads will fork thier behavior since `compare_exchange_strong` will return `true` only for one thread. – Marek R Mar 05 '21 at 14:49
  • so for me it seems that in the end one of this thread's newSize will override another's one – Bruice Mar 05 '21 at 14:52
  • compare_exchange_strong will return true only for one thread. - but you have a loop, and on the second loop iteration it will return true – Bruice Mar 05 '21 at 14:53
  • 1
    @Bruice Did you miss that it is a `do{...} while` loop, not just a `while` loop? If the exchange fails then a new value for `newSize` is calculated based on what `lastSize` actually is before trying again. – Frodyne Mar 05 '21 at 14:53
  • oh, yes, sorry! Now it makes sense! – Bruice Mar 05 '21 at 14:55
  • @Bruice in described scenario yes, in second iteration of loop second thread will calculate new value and next call of `compare_exchange_strong` will succeeds since there is no competing thread. – Marek R Mar 05 '21 at 14:55
  • 2
    I'm _really_ confused by your last line. Surely within a loop is the time to use weak. And outside of a loop you must use strong. Can you link me to the Herb talk?! – Mike Vine Mar 05 '21 at 15:38
  • @MikeVine probably it's a typo and it should be vice versa – Bruice Mar 05 '21 at 15:58
  • 1
    @Bruice thats what I would expect, but his actual example uses a loop _and_ strong, so I was worried he'd (or I'd) misunderstood something. – Mike Vine Mar 05 '21 at 16:00
  • @MikeVine Not sure if this is correct talk https://youtu.be/c1gO9aB9nbs but this comes to my mind first when I think about this. – Marek R Mar 05 '21 at 16:21
  • @MikeVine Ok I've found it https://youtu.be/CmxkPChOcvw?t=477 you were right should be vice versa so I corrected my answer. – Marek R Mar 05 '21 at 16:26
  • CAS_weak vs. _strong is easy to explain if you know about hardware that provides atomics via an [LL/SC](https://en.wikipedia.org/wiki/Load-link/store-conditional) primitive, unlike x86. CAS_weak can be implemented as one LL/SC attempt, that can spuriously fail if another thread writes or maybe even reads anything in the same cache line (or larger region monitored) between the LL and SC. CAS_strong requires an LL/SC retry loop. That's the practical difference on real-world ISAs. – Peter Cordes Aug 10 '22 at 18:35
  • The spelling errors in your code are distracting. `tryAcuqire` should be `tryAcquire`. `LockFreeCuncuretSizeLimit` should be `LockFreeConcurrentSizeLimit` – Peter Cordes Aug 10 '22 at 18:37
  • This is a `fetch_sub` but only if it won't wrap past zero. Yeah, that's a useful application for CAS, rather than actually over-shooting and correcting, because this avoids other threads ever seeing a negative (or huge unsigned) value. – Peter Cordes Aug 10 '22 at 18:40