Reputation: 7574
I have a function that accesses(reads and writes to) a std::atomic<bool>
variable. I'm trying to understand the order of execution of instructions so as to decide whether atomic will suffice or if I've got to use mutexes here. The function is given below -
// somewhere member var 'executing' is defined as std::atomic<bool>`
int A::something(){
int result = 0;
// my intention is only one thread should enter next block
// others should just return 0
if(!executing){
executing = true;
...
// do some really long processing
...
result = processed;
executing = false;
}
return result;
}
I've read this page on cppreference which mentions -
Each instantiation and full specialization of the std::atomic template defines an atomic type. If one thread writes to an atomic object while another thread reads from it, the behavior is well-defined (see memory model for details on data races)
and on Memory model page the following is mentioned -
When an evaluation of an expression writes to a memory location and another evaluation reads or modifies the same memory location, the expressions are said to conflict. A program that has two conflicting evaluations has a data race unless either
both conflicting evaluations are atomic operations (see std::atomic)
one of the conflicting evaluations happens-before another (see std::memory_order)
If a data race occurs, the behavior of the program is undefined.
and slight below it reads -
When a thread reads a value from a memory location, it may see the initial value, the value written in the same thread, or the value written in another thread. See std::memory_order for details on the order in which writes made from threads become visible to other threads.
This is slightly confusing to me, which one of above 3 statements are actually happening here?
When I perform if(!executing){
is this instruction an atomic instruction here? and more important - is it guaranteed that no other thread will enter that if loop if one two threads will enter that if body since first one will set executing
to true
?
And if something's wrong with the mentioned code, how should I rewrite it so that it reflects original intention..
Upvotes: 5
Views: 2430
Reputation: 1130
The issue in your code is that the two atomic operations, load (for comparison) and save, are separated, allowing for a lot to happen between them:
if(!executing) { // <-- Load (for compare).
// (Several threads can reach here, before one of them will execute the next line.)
executing = true; // <-- Save.
.
.
.
}
--
The proper way to use, is to replace the two with a single atomic operation:
bool expected{ false };
if (executing_.compare_exchange_strong(expected, true)) { // expected: false, desired: true.
.
.
.
}
compare_exchange_strong
always accesses the contained value to read it, and if the comparison is true (contained value == expected) it then also exchanges it with the desired value and returns true
. But the entire operation is atomic.
or:
if (!executing_.exchange(true)) { // If previously false, it wons the race to take the lock
.
.
.
}
exchange
atomically replaces the underlying value with desired (a read-modify-write operation) and returns the value of the atomic variable before the call.
--
Example:
#include <atomic>
#include <mutex>
class A final
{
public:
int DoSomething() // Only one thread should enter next block; others should just return 0
{
int result{ 0 };
bool expected{ false };
if (executing_.compare_exchange_strong(expected, true)) { // expected: false, desired: true.
// Do some really long processing...
result = 5; // 5 for example. In reality, the result of the processing.
executing_ = false;
}
return result;
}
protected:
std::atomic_bool executing_{ false };
};
Upvotes: 3
Reputation: 67713
Can you use a std::atomic_bool to ensure mutual exclusion in a code block?
Yep. std::atomic_bool
is sufficient to implement a spinlock, which is simpler (although also less general and a worse default) than the std::mutex
in the other answer, and still more complicated than you actually need.
Your problem is here:
When I perform
if(!executing){
... is it guaranteed that no other thread will enter that if loop if one two threads will enter that if body since first one will setexecuting
totrue
?
Both the load (for comparison) and the store are individually atomic, but they're not a single atomic operation, which literally means they're divisible (by another operation in a different thread).
However, there is an atomic load+compare+store for exactly this purpose: compare_exchange
.
Your code should be something like:
int A::something() {
int result = 0;
// if executing is false,
// atomically set it to true
bool expect_executing = false;
if(executing.compare_exchange_strong(
expect_executing, true)) {
// enter the branch only if the exchange succeeded
// (executing was false and is now true)
// ... do some really long processing ...
result = processed;
executing = false;
}
return result;
}
You can do something similar with std::atomic_flag::test_and_set
, but I stuck with your existing type.
You may also be able to weaken the default (sequential consistency) ordering.
Upvotes: 2
Reputation: 409
If I understand correctly, you are trying to ensure that only one thread will ever execute a stretch of code at the same time. This is exactly what a mutex does. Since you mentioned that you don't want threads to block if the mutex is not available, you probably want to take a look at the try_lock()
method of std::mutex
. See the documentation of std::mutex.
Now to why your code does not work as intended: Simplifying a little, std::atomic guarantees that there will be no data races when accessing the variable concurrently. I.e. there is a well defined read-write order. This doesn't suffice for what you are trying to do. Just imagine the if branch:
if(!executing) {
executing = true;
Remember, only the read-write operations on executing
are atomic. This leaves at least the negation !
and the if
itself unsynchronized. With two threads, the execution order could be like this:
executing
(atomically), value is falseexecuting
, value = trueexecuting
(atomically), value is falseexecuting
to trueNow both threads have entered the branch.
I would suggest something along these lines:
std::mutex myMutex;
int A::something(){
int result = 0;
// my intention is only one thread should enter next block
// others should just return 0
if(myMutex.try_lock()){
...
// do some really long processing
...
result = processed;
myMutex.unlock();
}
return result;
}
Upvotes: 8