在 condition_variable::wait() 调用期间中断程序 (SIGINT),随后调用 exit(),导致程序冻结

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

我不确定我是否理解这个问题,所以我写了一个小示例程序来演示它:

#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;
}

您可以运行在线程序,在这里:https://onlinegdb.com/Bkjf-4RHP

上面的例子是一个简单的多线程应用程序,由两个线程组成。主线程等待条件变量,直到收到通知并且 this->ready 已设置为 true。第二个线程只是定期更新 this->ready 并通知主线程。最后,应用程序在主线程 上处理 SIGINT ,它会尝试执行干净关闭。

问题:

当 SIGINT 被触发时(通过 Ctrl+C),应用程序不会退出,尽管在 Application::shutdown() 中调用了 exit()

这就是我认为正在发生的事情:

  1. 主线程正在等待通知,因此被 this->cv.wait(lock, [this] () {return this->ready;});
  2. 阻塞
  3. 收到 SIGINT,wait() 调用被信号中断,导致调用信号处理程序。
  4. 信号处理程序调用 Application::shutdown(),后者随后调用 exit()。对 exit() 的调用无限期挂起,因为它正在尝试进行一些清理,但在 wait() 调用恢复之前无法实现(对此我不确定)。

我真的不确定最后一点,但这就是我认为是这样的原因:

  • 当我在 Application::shutdown() 中删除对 exit() 的调用并让 main() return 时,程序退出没有问题。
  • 当我用 abort() 替换对 exit() 的调用时,它在清理方面做得更少,程序退出时没有问题(因此这表明 exit() 执行的清理过程是导致冻结)。
  • 如果在主线程等待条件变量时发送 SIGINT,程序将顺利退出。

以上只是我遇到的问题的一个例子。在我的例子中,我需要在 shutdown() 中调用 exit(),并且 shutdown() 需要从信号处理程序中调用。到目前为止,我的选择似乎是:

  • 将所有信号处理移至专用线程。这将是一件很痛苦的事情,因为它需要重写代码以使我能够从拥有 Application 实例的线程的不同线程调用 Application::shutdown()。我还需要一种方法将主线程从 wait() 调用中拉出来,可能是通过向谓词添加一些 OR 条件。
  • 将对 exit() 的调用替换为对 abort() 的调用。这会起作用,但会导致堆栈不被展开(具体来说,Application 实例)。

我还有其他选择吗?有什么方法可以在调用 std::condition_variable::wait() 期间正确中断线程并从中断处理程序中退出程序?

[support.signal]/3 An evaluation is signal-safe unless it includes one of the following:

(3.1) — a call to any standard library function, except for plain lock-free atomic operations and functions explicitly identified as signal-safe.
...

A signal handler invocation has undefined behavior if it includes an evaluation that is not signal-safe.

您的程序出现未定义的行为。信号处理程序可以安全地执行的操作非常有限。

正如 Igor 所提到的,您实际上不能在信号处理程序中做太多事情。不过,您可以对 lock-free 原子变量进行操作,因此您可以修改代码来处理它。

我添加了它并做了一些其他更改,并对我在代码中建议的更改发表了评论:

#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();
}