Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does wrapping a std::atomic_flag in a getter/setter void its "atomicity"?

Say I have a class that contains a std::atomic_flag as private member, exposed through a getter. Something like the following (pseudo-code):

class Thing
{
private:

    std::atomic_flag ready = ATOMIC_FLAG_INIT;

public:

    isReady()
    {
        return ready.test_and_set(); 
    }
} 

My naive question is: does querying the flag through a method turn it into a non-atomic operation, being a function call non-atomic (or is it?)? Should I make my ready flag a public member and querying it directly?

like image 683
Ignorant Avatar asked May 31 '17 07:05

Ignorant


3 Answers

No, it doesn't. The test_and_set() operation itself is atomic, so it doesn't matter how deep different threads' call-stacks are.

To demonstrate this, consider the base case where the atomic_flag object is "exposed" directly:

static atomic_flag flag = ATOMIC_FLAG_INIT;

void threadMethod() {

    bool wasFirst = !flag.test_and_set();
    if( wasFirst ) cout << "I am thread " << this_thread::get_id() << ", I was first!"      << endl;
    else           cout << "I am thread " << this_thread::get_id() << ", I'm the runner-up" << endl;
}

If two threads enter threadMethod - with one thread (t1) slightly before the other (t2) then we can expect the console output to be the following (in the same order):

I am thread t1, I was first!
I am thread t2, I'm the runner-up

Now if both threads enter simultaneously, but t2 is a microsecond ahead of t1, but t2 then becomes slower than t1 as it writes to stdout, then the output would be:

I am thread t1, I'm the runner-up
I am thread t2, I was first!

...so the call to test_and_set was still atomic, even though the output is not necessarily in the expected order.

Now if you were to wrap flag in another method (not inlined, just to be sure), like so...

__declspec(noinline)
bool wrap() {
    return !flag.test_and_set();
}

void threadMethod() {

    bool wasFirst = wrap();
    if( wasFirst ) cout << "I am thread " << this_thread::get_id() << ", I was first!"      << endl;
    else           cout << "I am thread " << this_thread::get_id() << ", I'm the runner-up" << endl;
}

...then the program would not behave any differently - because the false or true return bool value from test_and_set() will still be in each thread's stacks. Ergo, wrapping a atomic_flag does not change its atomicity.

like image 168
Dai Avatar answered Nov 04 '22 01:11

Dai


The atomicity property of C++ atomics guarantees that an operation can not be broken in the middle. That is, for a second thread observing the atomic, it will either observe the state before the test_and_set or the state after the test_and_set. It is not possible for such a thread to sneak in a clear between the test and the set part.

However, this is only true for the operation itself. As soon as the test_and_set call has completed, all bets are off again. You should always assume that the thread executing the test_and_set might get pre-empted immediately after finishing that instruction, so you can not assume that any instruction executing after the test_and_set will still observe the same state.

As such, adding the function call does not get you into trouble here. Any instruction following the atomic one must assume that the state of the atomic variable could have changed in the meantime. Atomics take this into account by providing interfaces that are designed in a special way: For example, test_and_set returning the result of the test, because obtaining that information through a separate call would not be atomic anymore.

like image 5
ComicSansMS Avatar answered Nov 04 '22 03:11

ComicSansMS


No, the isReady() method would work exactly the same as direct test_and_set() call, that is atomically.

like image 2
syntagma Avatar answered Nov 04 '22 03:11

syntagma