如何加入阻塞IO的线程?

时间:2008-10-15 04:53:29

标签: c linux multithreading pthreads

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

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

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

这是在Linux上并使用pthreads。

编辑:我使用SIGIO和SIGALRM玩了一下,当我没有安装信号处理程序时,他们打破了阻塞IO,但是在控制台上给出了一条消息(“I / O可能”)但是当我安装时一个信号处理程序,为了避免该消息,它们不再破坏阻塞IO,因此线程不会终止。所以我有点回到第一步。

13 个答案:

答案 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我不确定