2008-10-15 57 views
23

我有一个线程在后台运行,以阻塞方式从输入设备读取事件,现在当我退出应用程序时,我想正确清理线程,但是我不能只运行pthread_join(),因为该线程永远不会因阻塞IO而退出。如何加入挂在阻塞IO上的线程?

我该如何正确地解决这种情况?我应该发送一个pthread_kill(theard,SIGIO)还是一个pthread_kill(theard,SIGALRM)来打破这个块?这是否是正确的信号?或者有另一种方法来解决这种情况,并让该子线程退出阻塞读取?

目前有点困惑,因为我的谷歌搜索没有找到解决方案。

这是在Linux上并使用pthreads。编辑:我在SIGIO和SIGALRM中玩过一段时间,当我没有安装信号处理程序时,它们会阻止IO,但在控制台上给出消息(“I/O possible”),但是当我安装一个信号处理程序,为了避免该消息,它们不再中断阻塞IO,因此线程不会终止。所以我回到第一步。

+0

QQQ似乎有正确答案,不幸的是有极少数的选票。 `pthread_cancel`是解决您的问题的方法。 – 2010-09-27 03:30:19

+0

只要线程仍然被阻塞,它不会造成任何伤害。问题是如果线程在你关闭的时候醒来。所以解决的办法是在阻塞线之后放置一些代码,以阻止线程执行任何操作*其他*如果正在关闭进程。 – 2011-09-28 17:15:12

+0

类似的问题和可能的解决方案有讨论:[文件描述符和多线程程序(http://www.ddj.com/hpc-high-performance-computing/212001285) – dmityugov 2008-11-10 12:58:48

回答

2

老问题,可以很好地得到一个新的答案,因为事情已经演变,并且现在有一项新技术可用于更好处理线程中的信号。

由于Linux内核2.6。22,系统提供了一个称为signalfd()新功能,可以用来打开一组给定的Unix信号的文件描述符(那些彻底杀死一个过程之外。)

// 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

2

正如你所说,我认为唯一的方法就是发送一个信号,然后适当地捕捉并处理它。替代品可能是SIGTERM,SIGUSR1,SIGQUIT,SIGHUP,SIGINT等。

您也可以在输入描述符上使用select(),以便只在准备好时才读取。您可以使用select(),例如一秒的超时,然后检查该线程是否应该完成。

3

我上次遇到类似问题时发生的一种解决方案是创建一个仅用于唤醒阻塞线程的文件(例如管道)。

这个想法是从主循环创建一个文件(或每个线程1个,因为超时提示 - 这会让您更好地控制哪些线程被唤醒)。所有在文件I/O上阻塞的线程都会执行select(),使用它们正在尝试操作的文件以及由主循环创建的文件(作为读取的成员文件描述符集)。这应该会使所有的select()调用返回。

需要将主循环中处理此“事件”的代码添加到每个线程。

如果主循环需要唤醒所有线程,它可以写入文件或关闭它。


我不能肯定地说,如果这个工程,作为重组意味着需要尝试它消失了。

9

即使您的select()不频繁,您的select()也可能会超时,以便在某种条件下优雅退出线程。我知道,轮询很糟糕...

另一种替代方法是为每个孩子都有一个管道,并将其添加到线程正在监视的文件描述符列表中。当您希望退出该子项时,从父项向管道发送一个字节。不用每个线程的管道成本轮询。

6

取决于它如何等待IO。

如果线程处于“不间断IO”状态(在顶部显示为“D”),那么确实没有什么可以做的。线程通常只是简单地进入这个状态,做一些事情,比如等待页面被交换(或者需要加载,例如从mmap'd文件或共享库等),但是一个故障(特别是NFS服务器)可能会导致它会在这个状态下停留更长时间。

真的没有办法逃离这个“D”状态。线程不会响应信号(您可以发送它们,但它们将排队)。

如果它是一个正常的IO函数,如read(),write()或像select()或poll()这样的等待函数,则信号将正常传递。

1

我总是添加与我运行之前加入,以确保该线程将是合理的时间内可连接线程功能的“”功能。当一个线程使用阻塞IO时,我尝试利用系统来破解锁。例如,当我使用套接字时,我将终止关闭(2)关闭(2)或关闭(2),这会导致网络堆栈干净地终止它。

Linux的套接字实现是线程安全的。

0

根据不同的手册页,信号和线程在Linux上是一个微妙的问题。 您是否使用LinuxThreads或NPTL(如果您在Linux上)?

我不确定这一点,但我认为信号处理程序会影响整个过程,所以要么终止整个过程,要么终止所有过程。

你应该使用定时选择或轮询,并设置一个全局标志来终止你的线程。

13

我也会推荐使用选择或其他一些非信号手段来终止你的线程。我们有线索的原因之一是尝试摆脱信号疯狂。这就是说...

通常一个人使用pthread_kill()与SIGUSR1或SIGUSR2向线程发送信号。其他建议的信号 - 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); 

} 
0

我认为最干净方法会让线程在循环中使用条件变量来继续。

当发生I/O事件时,应该发送条件信号。

主线程可能只是在将循环谓词变为false时发出信号。

类似:

while (!_finished) 
{ 
    pthread_cond_wait(&cond); 
    handleio(); 
} 
cleanup(); 

记得用条件变量来妥善处理的信号。他们可以拥有诸如“虚假唤醒”之类的东西。所以我会围绕cond_wait函数包装自己的函数。

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我不知道

1

我很惊讶没有人提出过pthread_cancel。我最近编写了一个多线程I/O程序,并调用cancel()和join()之后工作得很好。

我最初尝试过pthread_kill(),但最终只是用我测试过的信号来终止整个程序。

1

如果您在EINTR上封闭的第三方库中,您可能需要考虑将pthread_kill与信号(USR1等)结合使用,调用一个空函数(而不是SIG_IGN),然后实际关闭/替换有问题的文件描述符。通过使用dup2将/ fd替换为/ dev/null或类似文件,您将使第三方库在重试读取时得到文件结束结果。

请注意,通过首先dup()原始套接字,可以避免需要实际关闭套接字。

12

执行此操作的规范方法是使用pthread_cancel,其中线程已完成pthread_cleanup_push/pop以为其正在使用的任何资源提供清理。

不幸的是,这不能在C++代码中使用。在pthread_cancel时,调用堆栈上的任何C++ std lib代码或ANY try {} catch()都可能会导致您的整个进程中断。

唯一的解决办法是处理SIGUSR1,设置一个停止标志,pthread_kill(SIGUSR1),那么任何地方线程被阻塞在I/O,如果你得到EINTR检查停止标志重试前的I/O。实际上,这在Linux上并不总是成功,不知道为什么。

但是在任何情况下,如果您必须调用任何第三方库,那么它们将毫无用处,因为它们很可能会有一个严格的循环,只需重新启动EINTR上的I/O即可。对其文件描述符进行逆向工程以关闭它也不会削减它 - 它们可能正在等待信号量或其他资源。在这种情况下,编写工作代码,句号根本不可能。是的,这完全是脑残。与那些设计C++例外和pthread_cancel的人交谈。据说这可能会在未来版本的C++中得到修复。祝你好运。