std::atomic<int> memory_order_relaxed VS volatile sig_atomic_t in a multithreaded program std::atomic<int> memory_order_relaxed VS volatile sig_atomic_t in a multithreaded program multithreading multithreading

std::atomic<int> memory_order_relaxed VS volatile sig_atomic_t in a multithreaded program


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.