【问题标题】:How to let a thread which blocks on recv() exit gracefully?如何让在 recv() 上阻塞的线程优雅地退出?
【发布时间】:2013-08-18 11:54:12
【问题描述】:

有这样一个话题:

{  
    ......    
    while (1)
    {
        recv(socket, buffer, sizeof(buffer), 0);
        ......
    }
    close(socket);           
}  

由于线程在 recv() 调用时被阻塞,我怎样才能让线程优雅地退出?

【问题讨论】:

  • 测试recvbreak的返回值是0还是负数?
  • 如果没有得到响应,是否有办法让 recv() 调用在设定的时间间隔后超时?

标签: c multithreading sockets unix


【解决方案1】:

您可以致电:-

shutdown(sock, SHUT_RDWR)    //on the remote end

签出this

【讨论】:

  • 远程端可能在另一个主机上的另一个进程中。
  • @MaximEgorushkin 他正在处理 this 端的套接字。
【解决方案2】:

不要在 recv 上阻塞。而是在 'select' 上实现一个块并测试输入 fdset 以确定是否首先要读取任何内容。

如果有数据要读取,调用recv。如果不是,则循环,检查启动关闭时由另一个线程设置的“正在运行”易失性布尔变量。

【讨论】:

  • 其实很高。它允许您为每个线程同时服务多个套接字。我在托管数千个连接的服务器上使用了这种方法。通过每个线程选择来管理 64 个套接字的块。即使在全部考虑下,CPU 负载仍然保持在不到百分之十。
  • :谢谢!根据我的理解,您的程序模型不是每个线程都服务于每个套接字,而是一个线程服务于多个套接字。你认为每个线程服务每个套接字的效率更高吗?
  • 不,我没有。然而,基于网络应用程序,正确的每线程套接字加载是可变的。尽管如此,平均而言,“一堆”套接字通常在等待硬件传送数据。根据连接的性质,某些套接字在统计上可能(根据我的经验)是空闲的。即使在一个线程中批处理的所有套接字有时也会休眠。单个线程等待一堆硬件中断比为每个套接字服务一个线程更有效。
  • 好的,谢谢您的详细解释!如果可能的话,您能否提供您的程序模型源代码供我参考?如果它是机密的,请保留它。
  • 请解释为什么您认为它不太好。 volatile 部分在多线程环境中是必要的,以确保 recv 线程获得 'isrunning' 标志的最新状态。如果另一个线程在 recv 线程上发出“停止”调用,这可以保证 recv 线程迅速行动,而不是耗尽可能缓存的 bool 值。
【解决方案3】:

要么:

  1. 使用setsockopt()SO_RCVTIMEO 设置读取超时,并在触发时检查状态变量,看看您是否告诉自己停止读取。
  2. 如果您想永远停止读取套接字,请使用shutdown(sd, SHUT_RD) 将其关闭以进行输入。这将导致 recv() 从现在开始返回零。
  3. 将套接字设置为非阻塞模式并使用select() 和超时来告诉您何时读取,采用与上述(1)中的状态变量相同的策略。但是,非阻塞模式会给发送操作带来相当大的复杂性,因此您应该更喜欢上面的 (1) 或 (2)。

您的伪代码缺少 EOS 和错误检查。我希望它不是真的那样。

【讨论】:

    【解决方案4】:

    声明一些“停止”布尔值,在每次 recv() 返回后检查它,如果设置则终止。要关闭,请设置布尔值并从另一个线程关闭套接字。阻塞的 recv() 将“立即”返回一个错误,但这并不重要,因为无论如何你都将要终止:)

    【讨论】:

    • 如果线程自己关闭了套接字,请注意这里潜在的竞争条件:那么另一个线程对 close() 的调用可能会意外关闭随后使用相同套接字号创建的其他套接字。
    • 如果您的操作系统在收到信号或类似古怪的东西后没有重新启动系统调用,那么向另一个线程发送信号可能会更好。
    • 建议另一个线程(设置布尔值的线程)使用关闭而不是关闭来改进这个答案。使用 close 是危险的,因为如果对 close 的调用发生在工作线程上的 recv 之前,那么文件描述符可能会被回收,并且 recv 将接收应用程序数据的不同部分,而不是程序员可能期望的 EBADFD。跨度>
    【解决方案5】:

    我可能会使用 @alk 的罚款 answer(也讨论过 here)中的信号。

    或者,您可以使用多路复用 I/O。

    在初始化时,创建一个全局管道(2)。当需要结束程序时,关闭管道的写端——现在读端将立即 select(2)/poll(2) 可读(对于 EOF)。同时,让你的recv-blocked线程包含这个管道的读取端以及它们的套接字,例如,无限期阻塞的select(2)调用。如果管道的读取端返回可读,则 I/O 线程知道是时候优雅地终止了。

    这种技术的主要警告是确保写入端只关闭一次,因为随后的天真的 close(2) 可能会 nip 一些无辜的文件,这些文件恰好被赋予与管道的旧写入相同的描述符-结束。

    【讨论】:

    • 这称为self-pipe trick。该解决方案不受与信号相关的竞争条件的影响。另一种变体是创建一个未绑定的SOCK_DGRAM 套接字并简单地将其关闭以发出关闭信号。
    • @rustyx,不,我的回答不涉及信号。管道如何关闭——信号、专用管理线程等——取决于实现者。此外,DJB 最初的用途是将字节写入管道,以免丢失信号,但这不是这里的问题。
    【解决方案6】:

    声明一个全局退出标志:

    int bExit = 0;
    

    让关键部分测试一下:

     while(1)
     {
       ssize_t result = recv(socket, buffer, sizeof(buffer), 0);
       if ((-1 == result) && (EINTR == error) && bExit) 
       {
         break;
       }
    
       ...
     }
    

    要破坏你的阅读器,首先设置退出标志

    bExit = 1;
    

    然后向阅读器线程发送信号

    pthread_kill(pthreadReader, SIGUSR1);
    

    注意: 我在这个例子中遗漏了保护bExit 防止并发访问。

    这可以通过使用互斥体或适当的声明来实现。

    【讨论】:

    • 请注意:如果对以下接口之一的阻塞调用(包括recv)被信号处理程序中断,则调用将在信号处理程序返回后自动重新启动,如果使用了 SA_RESTART 标志;否则调用将失败并返回错误 EINTR。 此外,您将 errnorecv() 返回值混淆了。
    • @MaximYegorushkin:感谢您指出result/errno 的困惑.. 我不知何故仍然使用pthread_*-API,它返回errno-values 但设置errno本身。关于SA_RESTART的提示也很重要,我错过了,再次感谢这个。
    • 这总是有竞争条件,对吧?因为一个线程中的pthread_kill 可能会在另一个线程中对recv 的调用之前运行,从而使您陷入困境。 (这场竞赛就是为什么发明像pselect 这样的调用的原因;您需要一种方法来解锁信号并自动进入系统调用,以便EINTR 方法能够可靠地工作。)
    【解决方案7】:

    另外,将您的套接字设置为非阻塞:

    int listen_sd = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP);
    if (listen_sd < 0) {
        perror("socket() failed");
    }
    
    int on = 1;
    
    //set socket to be non-blocking
    int rc = ioctl(listen_sd, FIONBIO,(char *)&on);
    if (rc < 0) {
        perror("ioctl() failed");
        close(listen_sd);
    }
    

    然后,您可以在套接字上调用 recv(),它不会阻塞。如果没有可读取的内容,则将 ERRNO 全局变量设置为常量 EWOULDBLOCK

    int rc = recv(listen_sd, buffer, sizeof(buffer), 0);
    if (rc < 0) {
        if (errno != EWOULDBLOCK) {
            perror("recv() failed");
        }
    }
    

    【讨论】:

    • if (errno != EWOULDBLOCK) { 最好是if ((errno != EWOULDBLOCK) &amp;&amp; (errno != EAGAIN)) {
    • @alk 当它们都被定义时,它们具有相同的值。
    • Sometimes EWOULDBLOCK != EAGAIN... 无关:设置非阻塞的 POSIX 方法是使用 fcntl(...O_NONBLOCK)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-29
    • 1970-01-01
    • 2013-07-23
    相关资源
    最近更新 更多