如何在另一个线程的 UDP 套接字上便携可靠地 unblock/interrupt `recv`?

How do I portably and reliably unblock/interrupt `recv` on a UDP socket from another thread?

问题

循环中阻塞的 UDP 套接字上有一个线程 运行 recv。我们需要停止该线程并关闭该套接字。

示例:

#include <unistd.h>
#include <sys/socket.h>

#include <thread>
#include <atomic>

std::atomic<int> the_socket;

void threadf() {
    for(;;) {
        char c;
        int ret = recv(the_socket, &c, 1, 0);
        if(ret == 0 || ret == -1) {
            break;
        }
    }
    close(the_socket);
}

int main() {
    the_socket = socket(AF_INET, SOCK_DGRAM, 0);
    std::thread *t = new std::thread(threadf);
    usleep(200000);
    shutdown(the_socket, SHUT_RDWR); // ENOTCONN
    t->join();
    delete t;
    return 0;
}

描述:

  1. 它创建 UDP 套接字
  2. 它启动另一个线程,从该套接字同步接收数据包。
  3. 主线程决定停止。 delete t 是个坏主意。 close(the_socket) 不会解除对相邻线程的阻塞。但是 shutdown,虽然它本身失败了,但它完成了工作:
[pid 22289] nanosleep({tv_sec=0, tv_nsec=200000000},  <unfinished ...>
[pid 22290] <... set_robust_list resumed> ) = 0
[pid 22290] recvfrom(3,  <unfinished ...>
[pid 22289] <... nanosleep resumed> NULL) = 0
[pid 22289] shutdown(3, SHUT_RDWR)      = -1 ENOTCONN (Transport endpoint is not connected)
[pid 22290] <... recvfrom resumed> "", 1, 0, NULL, NULL) = 0

问题


更新:似乎在 Mac 上失败了。 close 然而在那里确实有效...

仅考虑到 UDP 套接字本身,唯一真正可移植的唤醒线程的选项是:

  1. 将套接字切换到非阻塞模式,然后让线程使用select()(e)poll()来检测何时可以从套接字读取入站数据包。这样,您可以在每次等待时指定超时,并且线程可以在等待之间检查是否终止。

  2. 使用sendto()向socket的监听端口发送一个私有的UDP包来唤醒一个阻塞的recv()/recvfrom()调用,那么线程就会免费检查终止。请注意,如果您使用 connect() 将 UDP 套接字与对等点 IP/port 静态关联,这将不起作用,因为这样做会限制 recv()/recvfrom() 从接收来自其他来源。在您提供的示例中情况并非如此,但您应该注意这一点。

在许多平台上,您可以通过 setsockopt(SO_RCVTIMEO) 使用阻塞读取超时,然后您可以为每次读取指定一个超时,并且线程可以在读取之间检查是否终止。但并非所有平台都支持 SO_RCVTIMEO.

否则,您可以打开第二个 UDP 套接字或管道,并让线程使用 select()(e)poll() 与主 UDP 套接字同时监视它。不要在主套接字上调用 recv()/recvfrom() 除非确实有东西可以从中读取。 Connect/send 到第二个 pipe/socket 当你想唤醒线程时。