我正在尝试将使用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 /处理代码中实现同步?
答案 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
)。
从单个数据源读取多个线程并非标准做法,至少我认为这是不好的做法。
如果你想使用多个线程,但只有一个输入源,那么指定一个线程来监听和排队数据,这样其他线程就可以读取队列中的各个部分。