29

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

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

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

这是在 Linux 上并使用 pthreads。

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

4

13 回答 13

18

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

不幸的是,这永远不能在 C++ 代码中使用。任何 C++ 标准库代码,或try {} catch()当时调用堆栈上的任何代码pthread_cancel都可能 segvi 杀死你的整个进程。

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

但无论如何,谈论是否必须调用任何 3rd 方库是没有用的,因为它们很可能有一个紧密的循环,只会在EINTR. 对他们的文件描述符进行逆向工程以关闭它也不会切断它——他们可能正在等待信号量或其他资源。在这种情况下,根本不可能编写工作代码。是的,这完全是脑残。与设计 C++ 异常和pthread_cancel. 据说这可能会在未来的 C++ 版本中得到修复。祝你好运。

于 2010-09-26T22:08:01.400 回答
15

我也建议使用 select 或其他一些非基于信号的方法来终止你的线程。我们拥有线程的原因之一是试图摆脱疯狂的信号。那就是说...

通常使用 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);

}
于 2008-10-15T08:29:21.683 回答
9

为了在特定条件下优雅地退出线程,您select()可能会有超时,即使它不经常发生。我知道,民意调查很糟糕...

另一种选择是为每个孩子设置一个管道,并将其添加到线程正在监视的文件描述符列表中。当您希望该子级退出时,从父级向管道发送一个字节。不以每个线程的管道为代价进行轮询。

于 2008-10-15T06:54:36.210 回答
6

取决于它如何等待 IO。

如果线程处于“Uninterruptible IO”状态(在顶部显示为“D”),那么您真的无能为力。线程通常只是短暂地进入这个状态,做一些事情,比如等待页面被交换(或按需加载,例如从 mmap'd 文件或共享库等),但是失败(特别是 NFS 服务器)可能导致它会在那个状态停留更长时间。

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

如果是普通的 IO 函数,如 read()、write() 或等待函数,如 select() 或 poll(),信号将正常传递。

于 2008-10-15T06:57:03.367 回答
6

随着事情的发展和新技术可以更好地处理线程中的信号,老问题很可能会得到新的答案。

从 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 signals
f_socket = signalfd(-1, &set, SFD_NONBLOCK | SFD_CLOEXEC);

现在您可以使用poll()orselect()函数沿您正在侦听的更常用的文件描述符(套接字、磁盘上的文件等)侦听信号。

如果您想要一个可以一遍又一遍地检查信号和其他文件描述符的循环(即,它对您的其他文件描述符也很重要),那么 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等库感兴趣

于 2016-04-26T03:18:42.093 回答
3

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

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

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

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


我不能确定这是否有效,因为重组意味着尝试它的需要消失了。

于 2008-10-15T06:53:56.540 回答
2

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

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

于 2008-10-15T05:47:47.250 回答
1

我总是添加一个与我在加入之前运行的线程函数相关的“ kill ”函数,以确保线程可以在合理的时间内加入。当线程使用阻塞 IO 时,我会尝试利用系统来打破锁定。例如,当使用套接字时,我会在其上执行 kill call shutdown(2)close(2),这将导致网络堆栈干净地终止它。

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

于 2008-10-15T07:43:01.760 回答
1

我很惊讶没有人建议 pthread_cancel。我最近写了一个多线程 I/O 程序,然后调用 cancel() 和 join() 工作得很好。

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

于 2008-10-25T23:26:06.277 回答
1

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

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

于 2009-06-16T20:28:33.907 回答
0

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

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

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

于 2008-10-15T07:57:20.843 回答
0

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

当触发 i/o 事件时,应发出条件信号。

主线程可以在将循环谓词更改为 false 时发出条件信号。

就像是:

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

请记住使用条件变量来正确处理信号。他们可以有诸如“虚假唤醒”之类的东西。所以我会在 cond_wait 函数周围包装你自己的函数。

于 2008-10-15T13:58:03.283 回答
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 部分,您需要确保您使用 open 与 O_NOBLOCK 选项,或者如果它是一个套接字,您可以设置 MSG_NOWAIT?? 类似的标志。对于其他 fds 我不确定

于 2008-10-15T16:07:22.663 回答