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?
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.
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.
No, the isReady()
method would work exactly the same as direct test_and_set()
call, that is atomically.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With