我在后台运行一个以阻塞方式从输入设备读取事件的线程,现在当我退出应用程序时我想正确地清理线程,但我不能只运行pthread_join()因为线程永远不会因阻塞IO而退出。
如何正确解决这种情况?我应该发送pthread_kill(theard,SIGIO)还是pthread_kill(theard,SIGALRM)来打破阻塞?是其中任何一个甚至是正确的信号?或者是否有另一种方法来解决这种情况并让子线程退出阻塞读取?
目前有点疑惑,因为我的谷歌搜索都找不到解决方案。
这是在Linux上并使用pthreads。
编辑:我使用SIGIO和SIGALRM玩了一下,当我没有安装信号处理程序时,他们打破了阻塞IO,但是在控制台上给出了一条消息(“I / O可能”)但是当我安装时一个信号处理程序,为了避免该消息,它们不再破坏阻塞IO,因此线程不会终止。所以我有点回到第一步。
答案 0 :(得分:15)
执行此操作的规范方法是使用pthread_cancel
,其中线程已完成pthread_cleanup_push
/ pop
以便为正在使用的任何资源提供清理。
不幸的是,这不能在C ++代码中使用。 try {} catch()
时调用堆栈上的任何C ++ std lib代码或ANY pthread_cancel
都可能会导致整个进程被终止。
唯一的解决方法是处理SIGUSR1
,设置一个停止标志pthread_kill(SIGUSR1)
,然后在I / O上阻塞线程的任何地方,如果你得到EINTR
之前检查停止标志重试I / O.在实践中,这并不总是在Linux上成功,不知道为什么。
但无论如何,谈论你是否必须调用任何第三方lib是没用的,因为他们很可能会有一个简单的循环,只需重新启动EINTR
上的I / O.反向工程他们的文件描述符以关闭它也不会削减它 - 他们可能正在等待信号量或其他资源。在这种情况下,根本不可能编写工作代码,句点。是的,这完全是脑损伤。与设计C ++异常和pthread_cancel
的人交谈。据推测,这可以在将来的C ++版本中修复。祝你好运。
答案 1 :(得分:14)
我也建议使用select或其他一些非基于信号的方法来终止你的线程。我们有线程的原因之一是试图摆脱信号疯狂。那说......
通常,使用带有SIGUSR1或SIGUSR2的pthread_kill()向线程发送信号。其他建议的信号 - SIGTERM,SIGINT,SIGKILL - 具有您可能不感兴趣的进程范围的语义。
至于发送信号时的行为,我的猜测是它与你处理信号的方式有关。如果未安装处理程序,则应用该信号的默认操作,但是在接收信号的线程的上下文中。例如,SIGALRM将由您的线程“处理”,但处理将包括终止进程 - 可能不是所需的行为。
线程接收信号通常会将其从EINTR的读取中分离出来,除非它确实处于前面回答中提到的那种不间断状态。但我认为不是,或者您使用SIGALRM和SIGIO的实验不会终止该过程。
您的阅读是否可能在某种循环中?如果读取以-1返回终止,则跳出该循环并退出该线程。
你可以使用这个非常草率的代码来测试我的假设 - 我现在距离我的POSIX书籍还有几个时区......
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <signal.h>
int global_gotsig = 0;
void *gotsig(int sig, siginfo_t *info, void *ucontext)
{
global_gotsig++;
return NULL;
}
void *reader(void *arg)
{
char buf[32];
int i;
int hdlsig = (int)arg;
struct sigaction sa;
sa.sa_handler = NULL;
sa.sa_sigaction = gotsig;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(hdlsig, &sa, NULL) < 0) {
perror("sigaction");
return (void *)-1;
}
i = read(fileno(stdin), buf, 32);
if (i < 0) {
perror("read");
} else {
printf("Read %d bytes\n", i);
}
return (void *)i;
}
main(int argc, char **argv)
{
pthread_t tid1;
void *ret;
int i;
int sig = SIGUSR1;
if (argc == 2) sig = atoi(argv[1]);
printf("Using sig %d\n", sig);
if (pthread_create(&tid1, NULL, reader, (void *)sig)) {
perror("pthread_create");
exit(1);
}
sleep(5);
printf("killing thread\n");
pthread_kill(tid1, sig);
i = pthread_join(tid1, &ret);
if (i < 0)
perror("pthread_join");
else
printf("thread returned %ld\n", (long)ret);
printf("Got sig? %d\n", global_gotsig);
}
答案 2 :(得分:9)
您的select()
可能会超时(即使它很少),以便在特定条件下正常退出线程。我知道,民意调查很糟糕......
另一种方法是为每个子节点创建一个管道,并将其添加到线程正在监视的文件描述符列表中。当您希望该子项退出时,从父项向管道发送一个字节。不以每个线程的管道为代价进行轮询。
答案 3 :(得分:6)
取决于它如何等待IO。
如果线程处于“Uninterruptible IO”状态(顶部显示为“D”),那么你真的无能为力。线程通常只是短暂地进入这种状态,做一些事情,比如等待页面被交换(或者需求加载,例如来自mmap'd文件或共享库等),但是失败(特别是NFS服务器)可能导致它会在那个州停留更长的时间。
真的没有办法摆脱这种“D”状态。线程不会响应信号(你可以发送它们,但它们会排队)。
如果它是普通的IO函数,如read(),write()或者像select()或poll()这样的等待函数,信号将正常传递。
答案 4 :(得分:5)
老问题很可能会随着事情的发展得到一个新的答案,现在可以使用新的技术更好地处理线程中的信号。
自Linux内核2.6.22以来,系统提供了一个名为signalfd()
的新函数,可用于打开给定的一组Unix信号的文件描述符(在完全杀死进程的那些信号之外)。< / p>
// defined a set of signals
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
// ... you can add more than one ...
// prevent the default signal behavior (very important)
sigprocmask(SIG_BLOCK, &set, nullptr);
// open a file descriptor using that set of Unix signal
f_socket = signalfd(-1, &set, SFD_NONBLOCK | SFD_CLOEXEC);
现在,您可以使用poll()
或select()
函数来侦听您正在侦听的更常见的文件描述符(套接字,磁盘上的文件等)上的信号。
如果你想要一个可以反复检查信号和其他文件描述符的循环(即它对你的其他文件描述符也很重要),NONBLOCK很重要。
我有这样的实现,它适用于(1)定时器,(2)套接字,(3)管道,(4)Unix信号,(5)常规文件。实际上,真的是任何文件描述符加上计时器。
https://github.com/m2osw/snapcpp/blob/master/snapwebsites/libsnapwebsites/src/snapwebsites/snap_communicator.cpp
https://github.com/m2osw/snapcpp/blob/master/snapwebsites/libsnapwebsites/src/snapwebsites/snap_communicator.h
您可能也对libevent
等图书馆感兴趣答案 5 :(得分:3)
上次遇到这样的问题时,我遇到的一个解决方案是创建一个仅用于唤醒阻塞线程的文件(例如管道)。
这个想法是从主循环创建一个文件(或者每个线程1个,如超时所示 - 这可以让你更好地控制唤醒哪些线程)。在文件I / O上阻塞的所有线程都会使用他们尝试操作的文件执行select(),以及主循环创建的文件(作为读取的成员)文件描述符集)。这应该使所有select()调用返回。
需要将来自主循环处理此“事件”的代码添加到每个线程中。
如果主循环需要唤醒所有线程,它可以写入文件或关闭它。
我不能确定这是否有效,因为重组意味着尝试它的需要消失了。
答案 6 :(得分:2)
我认为,正如你所说,唯一的方法是发送信号,然后适当地捕捉并处理它。替代品可能是SIGTERM,SIGUSR1,SIGQUIT,SIGHUP,SIGINT等。
您还可以在输入描述符上使用select(),以便只在准备好时才能读取。你可以使用select(),超时,比如一秒,然后检查该线程是否应该完成。
答案 7 :(得分:1)
我总是添加一个与我在加入之前运行的线程函数相关的“ kill ”函数,以确保线程在合理的时间内可以加入。当线程使用阻塞IO时,我尝试利用系统来打破锁定。例如,当使用套接字时,我会杀死它上面的 shutdown(2)或 close(2),这将导致网络堆栈干净地终止它。
Linux'套接字实现是线程安全的。
答案 8 :(得分:1)
我很惊讶没有人建议pthread_cancel。我最近编写了一个多线程I / O程序并调用了cancel(),然后join()工作得非常好。
我最初尝试过pthread_kill()但最终只是用我测试的信号终止了整个程序。
答案 9 :(得分:1)
如果你在EINTR上循环的第三方库中阻塞,你可能想要考虑使用pthread_kill和一个调用空函数(不是SIG_IGN)的信号(USR1等)实际关闭/替换它的组合有问题的文件描述符。通过使用dup2将fd替换为/ dev / null或类似文件,您将导致第三方库在重试读取时获得文件结束结果。
请注意,首先通过dup()原始套接字,可以避免需要实际关闭套接字。
答案 10 :(得分:0)
根据不同的手册页,信号和线程在Linux上是一个微妙的问题。 你使用的是LinuxThreads还是NPTL(如果你使用的是Linux)?
我不确定这一点,但我认为信号处理程序会影响整个过程,因此要么终止整个过程,要么继续进行。
您应该使用timed select或poll,并设置一个全局标志来终止您的线程。
答案 11 :(得分:0)
我认为最干净的方法是让线程在循环中使用条件变量来继续。
当触发i / o事件时,应该发信号通知条件。
主线程可以在将循环谓词转换为false时发出信号。
类似的东西:
while (!_finished)
{
pthread_cond_wait(&cond);
handleio();
}
cleanup();
请记住使用条件变量来正确处理信号。他们可以拥有诸如“虚假唤醒”之类的东西。所以我会在cond_wait函数周围包装你自己的函数。
答案 12 :(得分:0)
struct pollfd pfd;
pfd.fd = socket;
pfd.events = POLLIN | POLLHUP | POLLERR;
pthread_lock(&lock);
while(thread_alive)
{
int ret = poll(&pfd, 1, 100);
if(ret == 1)
{
//handle IO
}
else
{
pthread_cond_timedwait(&lock, &cond, 100);
}
}
pthread_unlock(&lock);
thread_alive是一个特定于线程的变量,可以与信号结合使用来杀死线程。
至于句柄IO部分,你需要确保你使用O_NOBLOCK选项打开,或者如果它的套接字有一个类似的标志你可以设置MSG_NOWAIT ??。对于其他fds我不确定