在 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()
。
这就是我认为正在发生的事情:
- 主线程正在等待通知,因此被
this->cv.wait(lock, [this] () {return this->ready;});
阻塞
- 收到 SIGINT,
wait()
调用被信号中断,导致调用信号处理程序。
- 信号处理程序调用
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();
}
我不确定我是否理解这个问题,所以我写了一个小示例程序来演示它:
#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()
。
这就是我认为正在发生的事情:
- 主线程正在等待通知,因此被
this->cv.wait(lock, [this] () {return this->ready;});
阻塞
- 收到 SIGINT,
wait()
调用被信号中断,导致调用信号处理程序。 - 信号处理程序调用
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();
}