Search code examples
c++signalsatomiccondition-variablesigint

Is std::condition_variable::notify_one reentrant?


Can I safely execute following code? Is it possible to have deadlock here or some unexpected behaviour, especially when SIGINT arrives?

#include <atomic>
#include <condition_variable>
#include <csignal>

std::mutex m;
std::condition_variable cv;
std::atomic<bool> flag(false);

void f1(){
             std::signal(SIGTERM, [](int signal){flag.store(true, 
             std::memory_order_release); cv.notify_one();});//register for signal
             std::unique_lock<std::mutex> mtx(m);
             cv.wait(mtx, []{return flag.load(std::memory_order_consume);});//wait for signal or f2() notify
         }

void f2(){
             flag.store(true, std::memory_order_release);
             cv.notify_one();
         }

int main(){
    std::thread th1(f1);
    std::thread th2(f2);
    th1.join();
    th2.join();
    return 0;
}

Solution

  • pthread functions and std::condition_variable and std::mutex which use those are not async-signal safe. See the list of async-signal safe functions in man signal-safety(7).


    A bit off-topic: if you do not lock the mutex when updating flag then that leads to missed notifications. Imagine scenario:

    Thread 1        | Thread 2
                    | mutex.lock()
                    | flag == false
                    | cv.wait(mtx, []{return flag;}):
                    |     while(!flag)
    flag = true     | 
    cv.notify_one() | <--- this notification is lost
                    |         cv.wait(mtx); <--- waits for the next notification           
    

    This is a very subtle yet very common programming mistake. When the condition variable is notified often it is hard to notice the lost notifications and delays introduced. It is only when the condition variable notified only once the problem may become apparent.

    Condition variables don't have state, they are merely a (noisy) notification mechanism, unlike Windows Events, and that's a confounding factor.

    Locking the mutex when updating flag fixes this problem.


    If you would like to notify a condition variable when a signal is received create a thread dedicated to signal handling. Example:

    #include <condition_variable>
    #include <iostream>
    #include <thread>
    #include <signal.h>
    
    std::mutex m;
    std::condition_variable cv;
    bool flag = false;
    
    void f1(){
        std::unique_lock<std::mutex> lock(m);
        while(!flag)
            cv.wait(lock);
    }
    
    void signal_thread() {
        sigset_t sigset;
        sigfillset(&sigset);
        int signo = ::sigwaitinfo(&sigset, nullptr);
        if(-1 == signo)
            std::abort();
        std::cout << "Received signal " << signo << '\n';
    
        m.lock();
        flag = true;
        m.unlock();
        cv.notify_one();
    }
    
    int main(){
        sigset_t sigset;
        sigfillset(&sigset);
        ::pthread_sigmask(SIG_BLOCK, &sigset, nullptr);
    
        std::thread th1(f1);
        std::thread th2(signal_thread);
    
        th1.join();
        th2.join();
    }
    

    Note that the signals must be blocked in all threads, so that only the signal handler threads receives those.