c++multithreadingdeadlockcondition-variableresource-cleanup

Interrupting a program (SIGINT) during a condition_variable::wait() call, with a subsequent call to exit(), causes it to freeze


I'm not sure I understand this issue very well, so I've written a small example program that demonstrates it:

#include <iostream>
#include <csignal>
#include <mutex>
#include <condition_variable>
#include <thread>

class Application {
    std::mutex cvMutex;
    std::condition_variable cv;
    std::thread t2;
    bool ready = false;

    // I know I'm accessing this without a lock, please ignore that
    bool shuttingDown = false;

public:
    void mainThread() {
        auto lock = std::unique_lock<std::mutex>(this->cvMutex);

        while (!this->shuttingDown) {
            if (!this->ready) {
                std::cout << "Main thread waiting.\n" << std::flush;
                this->cv.wait(lock, [this] () {return this->ready;});
            }

            // Do the thing
            this->ready = false;
            std::cout << "Main thread notification recieved.\n" << std::flush;
        }
    };

    void notifyMainThread() {
        std::cout << "Notifying main thread.\n" << std::flush;
        this->cvMutex.lock();
        this->ready = true;
        this->cv.notify_all();
        this->cvMutex.unlock();
        std::cout << "Notified.\n" << std::flush;
    };

    void threadTwo() {
        while(!this->shuttingDown) {
            // Wait some seconds, then notify main thread
            std::cout << "Thread two sleeping for some seconds.\n" << std::flush;
            std::this_thread::sleep_for(std::chrono::seconds(3));
            std::cout << "Thread two calling notifyMainThread().\n" << std::flush;
            this->notifyMainThread();
        }

        std::cout << "Thread two exiting.\n" << std::flush;
    };

    void run() {
        this->t2 = std::thread(&Application::threadTwo, this);
        this->mainThread();

    };

    void shutdown() {
        this->shuttingDown = true;
        this->notifyMainThread();
        std::cout << "Joining thread two.\n" << std::flush;
        this->t2.join();
        std::cout << "Thread two joined.\n" << std::flush;
        // The following call causes the program to hang when triggered by a signal handler
        exit(EXIT_SUCCESS);
    }
};

auto app = Application();
int sigIntCount = 0;

int main(int argc, char *argv[])
{
    std::signal(SIGINT, [](int signum) {
        std::cout << "SIGINT recieved!\n" << std::flush;
        sigIntCount++;
        if (sigIntCount == 1) {
            // First SIGINT recieved, attempt a clean shutdown
            app.shutdown();
        } else {
            abort();
        }
    });

    app.run();

    return 0;
}

You can run the program online, here: https://onlinegdb.com/Bkjf-4RHP

The example above is a simple multithreaded application that consists of two threads. The main thread waits on a condition variable until a notification is received and this->ready has been set to true. The second thread simply updates this->ready and notifies the main thread, periodically. And finally, the application handles SIGINT on the main thread, where it attempts to perform a clean shutdown.

The issue:

When a SIGINT is triggered (via Ctrl+C), the application does not exit, despite calling exit() in Application::shutdown().

This is what I think is happening:

  1. The main thread is waiting for a notification, so it's being blocked by this->cv.wait(lock, [this] () {return this->ready;});
  2. SIGINT is received, and the wait() call is interrupted by the signal, which results in the signal handler being invoked.
  3. The signal handler calls Application::shutdown(), which subsequently calls exit(). The call to exit() hangs indefinitely because it's attempting some cleanup that cannot be achieved until the wait() call resumes (I'm not sure about this).

I'm really not sure about that last point, but this is why I think it's the case:

The above is just an example of the issue I'm having. In my case, I need to call exit() in shutdown(), and shutdown() needs to be invoked from the signal handler. So far, my options seem to be:

Do I have any other options? Is there any way to properly interrupt a thread during a call to std::condition_variable::wait(), and exit the program from within the interrupt handler?


Solution

  • As mentioned by Igor, you can't really do much in signal handlers. You may operate on lock-free atomic variables though, so you could modify the code to work on that.

    I've added that and made a few other changes and commented on my suggested changes in the code:

    #include <atomic>
    #include <condition_variable>
    #include <csignal>
    #include <iostream>
    #include <mutex>
    #include <thread>
    
    // Make sure the atomic type we'll operate on is lock-free.
    static_assert(std::atomic<bool>::is_always_lock_free);
    
    class Application {
        std::mutex cvMutex;
        std::condition_variable cv;
        std::thread t2;
        bool ready = false;
    
        static std::atomic<bool> shuttingDown;  // made it atomic
    
    public:
        void mainThread() {
            std::unique_lock<std::mutex> lock(cvMutex);
    
            while(!shuttingDown) {
                // There is no need to check  if(!ready)  here since
                // the condition in the cv.wait() lambda will be checked
                // before it is going to wait, like this:
                //
                // while(!ready) cv.wait(lock);
    
                std::cout << "Main thread waiting." << std::endl; // endl = newline + flush
                cv.wait(lock, [this] { return ready; });
                std::cout << "Main thread notification recieved." << std::endl;
    
                // Do the thing
                ready = false;
            }
        }
    
        void notifyMainThread() {
            { // lock scope - don't do manual lock() / unlock()-ing
                std::lock_guard<std::mutex> lock(cvMutex);
                std::cout << "Notifying main thread." << std::endl;
                ready = true;
            }
            cv.notify_all(); // no need to hold lock when notifying
        }
    
        void threadTwo() {
            while(!shuttingDown) {
                // Wait some seconds, then notify main thread
                std::cout << "Thread two sleeping for some seconds." << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(3));
                std::cout << "Thread two calling notifyMainThread()." << std::endl;
                notifyMainThread();
            }
            std::cout << "Time to quit..." << std::endl;
            notifyMainThread();
            std::cout << "Thread two exiting." << std::endl;
        }
    
        void run() {
            // Installing the signal handler as part of starting the application.
            std::signal(SIGINT, [](int /* signum */) {
                // if we have received the signal before, abort.
                if(shuttingDown) abort();
                // First SIGINT recieved, attempt a clean shutdown
                shutdown();
            });
    
            t2 = std::thread(&Application::threadTwo, this);
            mainThread();
    
            // move join()ing out of the signal handler
            std::cout << "Joining thread two." << std::endl;
            t2.join();
            std::cout << "Thread two joined." << std::endl;
        }
    
        // This is made static. All instances of Application
        // will likely need to shutdown.
        static void shutdown() { shuttingDown = true; }
    };
    
    std::atomic<bool> Application::shuttingDown = false;
    
    int main() {
        auto app = Application();
        app.run();
    }