在 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

本文关键字:调用 SIGINT exit 程序 condition variable wait 中断      更新时间:2024-05-10

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

#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. 收到 SIGINT,并且wait()调用被信号中断,从而导致调用信号处理程序。
  3. 信号处理程序调用Application::shutdown(),随后调用exit()。对exit()的调用无限期挂起,因为它正在尝试一些清理,直到wait()调用恢复才能实现(我不确定这一点)。

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

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

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

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

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

[support.signal]/3评估是信号安全的,除非它包含以下内容之一:

(3.1) — 对任何标准库函数的调用,纯无锁原子操作和函数除外 明确标识为信号安全.
...

如果信号处理程序调用包含非信号安全的评估,则它具有未定义的行为。

程序表现出未定义的行为。信号处理程序在安全方面非常有限。

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

我已经添加了它并进行了一些其他更改,并评论了我在代码中建议的更改:

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