Does volatile sig_atomic_t
give any memory order guarantees? E.g. if I need to just load/store an integer is it ok to use?
E.g. here:
volatile sig_atomic_t x = 0;
...
void f() {
std::thread t([&] {x = 1;});
while(x != 1) {/*waiting...*/}
//done!
}
is it correct code? Are there conditions it may not work?
Note: This is a over-simplifed example, i.e. I am not looking for a better solution for the given piece of code. I just want to understand what kind of behaviour I could expect from volatile sig_atomic_t
in a multithreaded program according to the C++ standard. Or, if it is a case, understand why behaviour is undefined.
I've found the following statement here:
The library type sig_atomic_t does not provide inter-thread synchronization or memory ordering, only atomicity.
And if I compare it with this definition here:
memory_order_relaxed: Relaxed operation: there are no synchronization or ordering constraints imposed on other reads or writes, only this operation's atomicity is guaranteed
Is it not the same? What does exactly atomicity mean here? Does volatile
do anything useful here? What's difference between "does not provide synchronization or memory ordering" and "no synchronization or ordering constraints"?
You are using an object of type sig_atomic_t
that is accessed by two threads (with one modifying).
Per the C++11 memory model, this is undefined behavior and the simple solution is to use std::atomic<T>
std::sig_atomic_t
and std::atomic<T>
are in different leagues.. In portable code, one cannot be replaced by the other and vice versa.
The only property that both share is atomicity (indivisible operations). That means that operations on objects of these types do not have an (observable) intermediate state, but that is as far as the similarities go.
sig_atomic_t
has no inter-thread properties. In fact, if an object of this type is accessed (modified) by more than one thread (as in your example code), it is technically undefined behavior (data race);
Therefore, inter-thread memory ordering properties are not defined.
what is sig_atomic_t
used for?
An object of this type may be used in a signal handler, but only if it is declared volatile
. The atomicity and volatile
guarantee 2 things:
For example:
volatile sig_atomic_t quit {0};
void sig_handler(int signo) // called upon arrival of a signal
{
quit = 1; // store value
}
void do_work()
{
while (!quit) // load value
{
...
}
}
Although this code is single-threaded, do_work
can be interrupted asynchronously by a signal that triggers sig_handler
and atomically changes the value of quit
.
Without volatile
, the compiler may 'hoist' the load from quit
out of the while loop, making it impossible for do_work
to observe a change to quit
caused by a signal.
Why can't std::atomic<T>
be used as a replacement for std::sig_atomic_t
?
Generally speaking, the std::atomic<T>
template is a different type because it is designed to be accessed concurrently by multiple threads and provides inter-thread ordering guarantees.
Atomicity is not always available at CPU level (especially for larger types T
) and therefore the implementation may use an internal lock to emulate atomic behavior.
Whether std::atomic<T>
uses a lock for a particular type T
is available through member function is_lock_free()
, or class constant is_always_lock_free
(C++17).
The problem with using this type in a signal handler is that the C++ standard does not guarantee that a std::atomic<T>
is lock-free for any type T
. Only std::atomic_flag
has that guarantee, but that is a different type.
Imagine above code where the quit
flag is a std::atomic<int>
that is not lock-free. There is a chance that when do_work()
loads the value,
it is interrupted by a signal after acquiring the lock, but before releasing it.
The signal triggers sig_handler()
which now wants to store a value to quit
by taking the same lock, which was already acquired by do_work
, oops. This is undefined behavior and possibly causes a dead-lock.
std::sig_atomic_t
does not have that problem because it does not use locking. All that is needed is a type that is indivisible at CPU level and on many platforms, it can be as simple as:
typedef int sig_atomic_t;
The bottom line is, use volatile std::sig_atomic_t
for signal handlers in a single thread and use std::atomic<T>
as a data-race-free type, in a multi threaded environment.