使用epoll_wait时如何正确读取数据

时间:2011-04-04 15:53:43

标签: c++ linux multithreading recv epoll

我正在尝试将使用IOCP的现有Windows C ++代码移植到Linux。在决定使用epoll_wait来实现高并发性之后,我已经面临一个理论问题,即我们何时尝试处理接收到的数据。

想象一下两个线程正在调用epoll_wait,并且收到了两个确认消息,这样Linux就会解锁第一个线程,很快就会解锁第二个线程。

示例:

Thread 1 blocks on epoll_wait
Thread 2 blocks on epoll_wait
Client sends a chunk of data 1
Thread 1 deblocks from epoll_wait, performs recv and tries to process data
Client sends a chunk of data 2
Thread 2 deblocks, performs recv and tries to process data.

这种情况可以想象吗?即会发生吗?

有没有办法阻止它,以避免在recv /处理代码中实现同步?

5 个答案:

答案 0 :(得分:5)

如果您有多个线程从同一组epoll句柄中读取,我建议您使用EPOLLONESHOT将epoll句柄置于单次触发级别触发模式。这将确保在一个线程观察到触发的句柄之后,在您使用epoll_ctl重新设置句柄之前,没有其他线程会观察到它。

如果需要独立处理读写路径,可能需要完全拆分读写线程池;有一个epoll句柄用于读取事件,一个用于写入事件,并将线程分配给一个或另一个。此外,还有一个用于读取和写入路径的单独锁定。当然,在修改任何每个套接字状态时,必须注意读写线程之间的交互。

如果你采用这种拆分方法,你需要考虑如何处理套接字闭包。您很可能需要一个额外的共享数据锁,以及在共享数据锁下设置的“确认闭包”标志,用于读取和写入路径。然后,读写线程可以竞争确认,最后一个确认可以清理共享数据结构。就是这样:

void OnSocketClosed(shareddatastructure *pShared, int writer)
{
  epoll_ctl(myepollhandle, EPOLL_CTL_DEL, pShared->fd, NULL);
  LOCK(pShared->common_lock);
  if (writer)
    pShared->close_ack_w = true;
  else
    pShared->close_ack_r = true;

  bool acked = pShared->close_ack_w && pShared->close_ack_r;
  UNLOCK(pShared->common_lock);

  if (acked)
    free(pShared);
}

答案 1 :(得分:3)

我在这里假设您尝试处理的情况是这样的:

您有多个(可能很多)套接字要同时接收数据;

您希望在第一次接收时从线程A上的第一个连接开始处理数据,然后确保来自此连接的数据不会在任何其他线程上处理,直到您在线程A中完成它为止。

当你这样做的时候,如果现在在不同的连接上接收到某些数据,你希望线程B选择该数据并对其进行处理,同时仍然确保没有其他人可以处理此连接,直到线程B完成它等

在这些情况下,事实证明在多个线程中使用epoll_wait()和相同的epoll fd是一种相当有效的方法(我并不是说它必然是效率最高的)。

这里的技巧是使用EPOLLONESHOT标志将各个连接fds添加到epoll fd。这确保了一旦从epoll_wait()返回fd,它就会被监视,直到您明确告诉epoll再次监视它。这可确保处理此连接的线程不会受到干扰,因为在此线程标记要再次监视的连接之前,其他线程无法处理相同的连接。

您可以使用epoll_ctl()和EPOLL_CTL_MOD设置fd以再次监视EPOLLIN或EPOLLOUT。

在多个线程中使用这样的epoll的一个显着好处是,当一个线程完成连接并将其添加回epoll监视集时,仍然在epoll_wait()中的任何其他线程都会立即监视它。处理线程返回epoll_wait()。顺便提一下,如果不同的线程现在立即获取该连接(因此需要获取此连接的数据结构并刷新先前线程的缓存),这也可能是缺点,因为缺少缓存数据局部性。什么最有效将敏感地取决于您的确切使用模式。

如果您尝试处理随后在不同线程中的同一连接上收到的消息,那么使用epoll的这种方案将不适合您,并且使用监听线程提供有效队列馈送工作线程的方法可能是更好。

答案 2 :(得分:2)

之前的答案指出,从多个线程调用epoll_wait()几乎肯定是正确的,但我对这个问题很感兴趣,试图找出当它从多个线程调用时会发生什么。相同的句柄,等待同一个套接字。我写了以下测试代码:

#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

struct thread_info {
  int number;
  int socket;
  int epoll;
};

void * thread(struct thread_info * arg)
{
    struct epoll_event events[10];
    int s;
    char buf[512];

    sleep(5 * arg->number);
    printf("Thread %d start\n", arg->number);

    do {
        s = epoll_wait(arg->epoll, events, 10, -1);

        if (s < 0) {
            perror("wait");
            exit(1);
        } else if (s == 0) {
            printf("Thread %d No data\n", arg->number);
            exit(1);
        }
        if (recv(arg->socket, buf, 512, 0) <= 0) {
            perror("recv");
            exit(1);
        }
        printf("Thread %d got data\n", arg->number);
    } while (s == 1);

    printf("Thread %d end\n", arg->number);

    return 0;
}

int main()
{
    pthread_attr_t attr;
    pthread_t threads[2];
    struct thread_info thread_data[2];
    int s;
    int listener, client, epollfd;
    struct sockaddr_in listen_address;
    struct sockaddr_storage client_address;
    socklen_t client_address_len;
    struct epoll_event ev;

    listener = socket(AF_INET, SOCK_STREAM, 0);

    if (listener < 0) {
        perror("socket");
        exit(1);
    }

    memset(&listen_address, 0, sizeof(struct sockaddr_in));
    listen_address.sin_family = AF_INET;
    listen_address.sin_addr.s_addr = INADDR_ANY;
    listen_address.sin_port = htons(6799);

    s = bind(listener,
             (struct sockaddr*)&listen_address,
             sizeof(listen_address));

    if (s != 0) {
        perror("bind");
        exit(1);
    }

    s = listen(listener, 1);

    if (s != 0) {
        perror("listen");
        exit(1);
    }

    client_address_len = sizeof(client_address);
    client = accept(listener,
                    (struct sockaddr*)&client_address,
                    &client_address_len);

    epollfd = epoll_create(10);
    if (epollfd == -1) {
        perror("epoll_create");
        exit(1);
    }

    ev.events = EPOLLIN;
    ev.data.fd = client;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client, &ev) == -1) {
        perror("epoll_ctl: listen_sock");
        exit(1);
    }

    thread_data[0].number = 0;
    thread_data[1].number = 1;
    thread_data[0].socket = client;
    thread_data[1].socket = client;
    thread_data[0].epoll = epollfd;
    thread_data[1].epoll = epollfd;

    s = pthread_attr_init(&attr);
    if (s != 0) {
        perror("pthread_attr_init");
        exit(1);
    }

    s = pthread_create(&threads[0],
                       &attr,
                       (void*(*)(void*))&thread,
                       &thread_data[0]);

    if (s != 0) {
        perror("pthread_create");
        exit(1);
    }

    s = pthread_create(&threads[1],
                       &attr,
                       (void*(*)(void*))&thread,
                       &thread_data[1]);

    if (s != 0) {
        perror("pthread_create");
        exit(1);
    }

    pthread_join(threads[0], 0);
    pthread_join(threads[1], 0);

    return 0;
}

当数据到达时,两个线程都在epoll_wait()上等待,只有一个会返回,但随着后续数据到达,唤醒处理数据的线程在两个线程之间实际上是随机的。我无法找到影响哪个线程被唤醒的方法。

调用epoll_wait的单个线程似乎最有意义,事件传递给工作线程以泵送IO。

答案 3 :(得分:1)

我相信使用epoll的高性能软件和每个核心的线程会创建多个epoll句柄,每个句柄处理所有连接的子集。通过这种方式,工作可以分开,但是避免了你描述的问题。

答案 4 :(得分:0)

通常,当您有一个线程侦听单个异步源上的数据时,会使用epoll。为了避免繁忙等待(手动轮询),您可以使用epoll告知数据何时准备就绪(非常像select)。

从单个数据源读取多个线程并非标准做法,至少我认为这是不好的做法。

如果你想使用多个线程,但只有一个输入源,那么指定一个线程来监听和排队数据,这样其他线程就可以读取队列中的各个部分。