【问题标题】:How to join a thread that is hanging on blocking IO?如何加入挂在阻塞 IO 上的线程?
【发布时间】:2010-09-17 06:05:53
【问题描述】:

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

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

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

这是在 Linux 上使用 pthreads。

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

【问题讨论】:

  • 那里讨论了类似的问题和可能的解决方案:File Descriptors And Multithreaded Programs
  • 这篇文章给了我我一直在寻找的东西——shutdown(fd, SHUT_RDWR);。谢谢。
  • qqq好像有正确答案,可惜票数很少。 pthread_cancel 是您问题的解决方案。
  • 只要线程保持阻塞状态,它就不会造成任何伤害。问题是线程是否在您关闭时唤醒。因此,解决方法是在停止线程执行任何操作else的行之后放置一些代码(如果正在进行关闭)。

标签: c linux multithreading pthreads


【解决方案1】:

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

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

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

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

【讨论】:

  • “实际上,这在 Linux 上并不总是成功,不知道为什么”是什么意思。 (@qqq)。可以连接到这个lwn.net/Articles/683118
【解决方案2】:

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

通常使用带有 SIGUSR1 或 SIGUSR2 的 pthread_kill() 来向线程发送信号。其他建议的信号——SIGTERM、SIGINT、SIGKILL——具有您可能不感兴趣的进程范围的语义。

至于你发送信号时的行为,我的猜测是它与你如何处理信号有关。如果您没有安装处理程序,则应用该信号的默认操作,但在接收信号的线程的上下文中。因此,例如,SIGALRM 将由您的线程“处理”,但处理将包括终止进程——可能不是所需的行为。

线程接收到信号通常会将其从使用 EINTR 的读取中中断,除非它确实处于前面答案中提到的那种不可中断状态。但我认为不是,否则您对 SIGALRM 和 SIGIO 的实验不会终止该过程。

您的阅读可能是在某种循环中吗?如果读取以 -1 return 终止,则跳出该循环并退出线程。

您可以使用我整理的这个非常草率的代码来测试我的假设——我现在距离我的 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);

}

【讨论】:

  • 你是对的,read() 实际上是在一个检查 EINTR 的 while 循环中,因为它在第三方库中,而不是我自己的代码中,我完全错过了这个事实,那就是为什么一个简单的信号没有达到我的预期。
  • 这种方法可以释放flockfile获取的资源吗?
【解决方案3】:

你的select() 可能有一个超时,即使它不经常发生,以便在特定条件下优雅地退出线程。我知道,投票很烂……

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

【讨论】:

  • 或者您可以为所有线程使用一个管道,“就绪”状态从选择/轮询返回到等待单个文件描述符的多个线程(只要它是级别触发的)。因此,在单个“杀手”管道上等待的所有线程都会收到死亡通知。
【解决方案4】:

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

从 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()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】:

    取决于它如何等待 IO。

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

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

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

    【讨论】:

      【解决方案6】:

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

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

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

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


      我不能确定这是否有效,因为重组意味着不再需要尝试。

      【讨论】:

        【解决方案7】:

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

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

        【讨论】:

          【解决方案8】:

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

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

          【讨论】:

            【解决方案9】:

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

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

            【讨论】:

              【解决方案10】:

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

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

              【讨论】:

                【解决方案11】:

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

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

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

                【讨论】:

                  【解决方案12】:

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

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

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

                  类似:

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

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

                  【讨论】:

                    【解决方案13】:
                    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 我不确定

                    【讨论】:

                      猜你喜欢
                      • 2014-01-20
                      • 1970-01-01
                      • 2013-10-05
                      • 1970-01-01
                      • 1970-01-01
                      • 2015-04-09
                      • 2020-07-19
                      • 2015-06-25
                      • 2011-05-24
                      相关资源
                      最近更新 更多