If you want to mimic the interrupt behavior on the other threads, those threads have to allow interruption, and your signal handling thread has to deliver the signal to them. Consider the following snippet:
static volatile std::atomic<bool> quit;
static volatile std::deque<std::thread> all;
static volatile pthread_t main_thread;
void sigint_handler (int) {
if (pthread_self() == main_thread) {
write(2, "\rQuitting.\n", 11);
quit = true;
for (auto &t : all) pthread_kill(t.native_handle(), SIGINT);
} else if (!quit) pthread_kill(main_thread, SIGINT);
}
Quitting is communicated by setting a global variable. A thread is to wakeup and check that variable. Waking up the thread is a matter of visiting the thread and sending it a signal.
In the event a worker thread intercepts SIGINT
before the main thread does, then it sends it to the main thread to initiate a proper shutdown sequence.
In order to allow being interrupted, a thread may call siginterrupt()
.
void readCanbus(int s) {
siginterrupt(SIGINT, 1);
while(!quit) {
char buf[256];
int nbytes = read(s, buf, sizeof(buf));
}
write(2, "Quit.\n", 6);
}
We define two ways of installing a signal handler, one using signal
, the other using sigaction
, but using a flag that provides semantics similar to signal
.
template <decltype(signal)>
void sighandler(int sig, sighandler_t handler) {
signal(sig, handler);
}
template <decltype(sigaction)>
void sighandler(int sig, sighandler_t handler) {
struct sigaction sa = {};
sa.sa_handler = handler;
sa.sa_flags = SA_RESTART;
sigaction(sig, &sa, NULL);
}
The main thread installs the signal handler, initializes main_thread
, and populates the container with the worker threads that need to be shutdown later.
int main () {
int sp[2];
main_thread = pthread_self();
socketpair(AF_UNIX, SOCK_STREAM, 0, sp);
all.push_back(std::thread(readCanbus, sp[0]));
sighandler<sigaction>(SIGINT, sigint_handler);
for (auto &t : all) t.join();
}