Я не уверен, что хорошо разбираюсь в этом вопросе, поэтому я написал небольшой пример программы, демонстрирующий это:
#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), приложение не закрывается, несмотря на вызов exit()
в Application::shutdown()
.
Вот что я думаю, происходит:
- Основной поток ожидает уведомления, поэтому он заблокирован
this->cv.wait(lock, [this] () {return this->ready;});
- Принимается сигнал SIGINT, и вызов
wait()
прерывается сигналом, в результате чего вызывается обработчик сигнала. - Обработчик сигнала вызывает
Application::shutdown()
, который впоследствии вызываетexit()
. Вызовexit()
зависает на неопределенное время, потому что он пытается выполнить некоторую очистку, которую нельзя выполнить, пока не возобновится вызовwait()
(я не уверен в этом).
Я действительно не уверен в этом последнем пункте, но вот почему я думаю, что это так:
- Когда я удаляю вызов
exit()
вApplication::shutdown()
и позволяюmain()
вернуться, программа завершается без проблем. - Когда я заменяю вызов
exit()
наabort()
, который меньше влияет на очистку, программа завершается без проблем (это указывает на то, что процесс очистки, выполняемый функцией exit(), приводит к зависанию). - Если SIGINT отправляется, когда основной поток не ожидает переменную условия, программа завершается без проблем.
Вышеупомянутое является лишь примером проблемы, с которой я сталкиваюсь. В моем случае мне нужно вызвать exit()
в shutdown()
, а shutdown()
нужно вызвать из обработчика сигнала. Пока мои варианты выглядят так:
- Переместите всю передачу сигналов в выделенный поток. Это было бы сложно сделать, так как потребовалось бы переписать код, чтобы я мог вызывать
Application::shutdown()
из другого потока, а не того, которому принадлежит экземплярApplication
. Мне также нужен способ вывести основной поток из вызоваwait()
, вероятно, добавив некоторое условиеOR
к предикату. - Замените вызов
exit()
вызовомabort()
. Это будет работать, но приведет к тому, что стек не будет раскручен (в частности, экземплярApplication
).
Есть ли у меня другие варианты? Есть ли способ правильно прервать поток во время вызова std::condition_variable::wait()
и выйти из программы из обработчика прерывания?