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:
- atomicity: A signal handler can asynchronously store a value to the object and anyone reading the same variable (in the same thread) can only observe the before- or after value.
- volatile: A store cannot be 'optimized away' by the compiler and is therefore visible (in the same thread) at (or after) the point where the signal interrupted the execution.
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 happens to be 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.
std::atomic_signal_fence
I'd assume there are no ordering guarantees for anything 'signal' related. – Esmevolatile
means "give me the semantic of the underlying CPU and memory system". So the Q is inherently impl dependent (in theory). In practice, yes, it's the same. – Cenesthesia