我正在编写一个小型服务器,它将从多个来源接收数据并处理这些数据。收到的消息来源和数据非常重要,但epoll应该能够很好地处理。但是,必须解析所有接收到的数据并运行大量的测试,这些测试非常耗时,并且尽管进行了epoll多路复用,仍会阻塞单个线程。基本上,模式应该如下所示:IO循环接收数据并将其捆绑到作业中,发送到池中可用的第一个线程,捆绑由作业处理,结果传递到IO循环写信给文件。
我决定选择一个IO线程和N个工作线程。使用以下提供的示例可轻松实现用于接受tcp连接和读取数据的IO线程: http://linux.die.net/man/7/epoll
线程通常也很容易处理,但我正在努力将epoll IO循环与线程池以优雅的方式结合起来。我无法找到任何在网上使用epoll的“最佳实践”,但有关同一主题的相关问题很多。
因此我有一些问题希望有人能帮我回答:
编辑:一种可能的解决方案是从IO循环更新环形缓冲区,更新后通过共享管道向所有工作人员发送环形缓冲区索引(从而将该索引的控制权交给第一个工作人员)从管道中读取索引),让工作者拥有该索引直到处理结束,然后再通过管道将索引号发送回IO线程,从而返回控制权?
我的应用程序仅限Linux,因此我可以使用仅限Linux的功能,以便以最优雅的方式实现此目的。不需要跨平台支持,但性能和线程安全性是。
答案 0 :(得分:6)
在我的测试中,每个线程一个epoll实例远远超过了复杂的线程模型。如果将侦听器套接字添加到所有epoll实例,则工作人员只需accept(2)
,获胜者将获得连接并在其生命周期内对其进行处理。
你的工人可能看起来像这样:
for (;;) {
nfds = epoll_wait(worker->efd, &evs, 1024, -1);
for (i = 0; i < nfds; i++)
((struct socket_context*)evs[i].data.ptr)->handler(
evs[i].data.ptr,
evs[i].events);
}
添加到epoll实例的每个文件描述符都可以与struct socket_context
关联:
void listener_handler(struct socket_context* ctx, int ev)
{
struct socket_context* conn;
conn->fd = accept(ctx->fd, NULL, NULL);
conn->handler = conn_handler;
/* add to calling worker's epoll instance or implement some form
* of load balancing */
}
void conn_handler(struct socket_context* ctx, int ev)
{
/* read all available data and process. if incomplete, stash
* data in ctx and continue next time handler is called */
}
void dummy_handler(struct socket_context* ctx, int ev)
{
/* handle exit condition async by adding a pipe with its
* own handler */
}
我喜欢这个策略,因为:
read(2)
; accept(2)
上的同步); accept(2)
。关于epoll的一些注释:
EAGAIN
; dup(2)
系列调用以避免一些意外(epoll注册文件描述符,但实际上会监视文件描述); epoll_ctl(2)
其他线程的epoll实例; struct epoll_event
使用较大的epoll_wait(2)
缓冲区以避免饥饿。其他一些说明:
accept4(2)
保存系统调用; poll(2)
/ select(2)
可能会更快。我希望这会有所帮助。
答案 1 :(得分:3)
执行此模型时,因为我们只有在完全接收到数据包后才知道数据包大小,遗憾的是我们无法将接收本身卸载到工作线程。相反,我们仍然可以做的最好的事情是接收数据的线程,该数据必须将指针传递给完全接收的数据包。
数据本身可能最好保存在循环缓冲区中,但是我们需要为每个输入源提供一个单独的缓冲区(如果我们得到一个部分数据包,我们可以继续从其他来源接收而不分割数据。剩下的问题是如何告知工作人员新数据包何时准备好,并给他们一个指向所述数据包中数据的指针。因为这里的数据很少,只有一些指针,最优雅的方法是使用posix消息队列。它们为多个发送者和多个接收者提供了写入和读取消息的能力,始终确保每个消息都被接收并且精确地通过一个线程。
对于每个数据源,您将需要一个类似下面的结构,我现在将完成字段目的。
struct DataSource
{
int SourceFD;
char DataBuffer[MAX_PACKET_SIZE * (THREAD_COUNT + 1)];
char *LatestPacket;
char *CurrentLocation
int SizeLeft;
};
SourceFD显然是有问题的数据流的文件描述符,DataBuffer是处理包时内容的地方,它是一个循环缓冲区。 LatestPacket指针用于临时保存指向最重新发送的数据包的指针,以防我们收到部分数据包并在关闭数据包之前移动到另一个源。 CurrentLocation存储最新数据包的结束位置,以便我们知道下一个数据包的放置位置或部分接收的位置。剩下的大小是缓冲区中留下的空间,这将用于判断我们是否可以适应数据包或需要绕回到开头。
接收功能因此将有效
工作线程将使用接收的指针进行处理,然后增加SizeLeft,以便接收器线程知道它可以继续填充缓冲区。原子函数将需要处理结构中的大小值,因此我们不会获得具有size属性的竞争条件(因为它可能由工作者和IO线程同时写入,导致丢失写入,请参阅下面的评论),它们列在here中并且非常简单且非常有用。
现在,我已经给出了一些一般背景知识,但将特别提到这些要点:
最后你的编辑是相当明智的,除了我所建议的事实,消息队列远比管道好,因为它们非常有效地发出事件信号,保证完整的消息读取并提供自动框架。
我希望这会有所帮助,但是这已经很晚了,所以如果我错过任何内容或者您有任何问题可以随意评论澄清或更多解释。
答案 2 :(得分:0)
我在其他帖子中发布了相同的答案:I want to wait on both a file descriptor and a mutex, what's the recommended way to do this?
=============================================== ===========
这是一个非常常见的问题,尤其是在开发网络服务器端程序时。大多数Linux服务器端程序的主要外观都会像这样循环:
epoll_add(serv_sock);
while(1){
ret = epoll_wait();
foreach(ret as fd){
req = fd.read();
resp = proc(req);
fd.send(resp);
}
}
它是单线程(主线程),基于epoll的服务器框架。问题是,它是单线程的,而不是多线程的。它要求proc()永远不会阻塞或运行很长时间(例如,对于常见情况,为10 ms)。
如果proc()将运行很长时间,我们需要MULTI THREADS,并在一个单独的线程(工作线程)中执行proc()。
我们可以在不阻塞主线程的情况下向工作线程提交任务,使用基于互斥锁的消息队列,它足够快。
然后我们需要一种从工作线程获取任务结果的方法。怎么样?如果我们只是在epoll_wait()之前或之后直接检查消息队列,但是,在epoll_wait()结束后执行检查操作,如果等待所有文件描述符,则epoll_wait()通常会阻塞10微秒(常见情况)不活跃。
对于服务器,10毫秒是相当长的时间!我们可以发信号通知epoll_wait()在生成任务结果时立即结束吗?
是的!我将在我的一个开源项目中描述它是如何完成的。
为所有工作线程创建管道,epoll也在该管道上等待。生成任务结果后,工作线程将一个字节写入管道,然后epoll_wait()将在几乎相同的时间内结束! - Linux管道有5到20美元的延迟。
在我的项目SSDB(Redis协议兼容的磁盘内NoSQL数据库)中,我创建了一个SelectableQueue,用于在主线程和工作线程之间传递消息。就像它的名字一样,SelectableQueue有一个文件描述符,可以通过epoll等待。
SelectableQueue:https://github.com/ideawu/ssdb/blob/master/src/util/thread.h#L94
主线程中的用法:
epoll_add(serv_sock);
epoll_add(queue->fd());
while(1){
ret = epoll_wait();
foreach(ret as fd){
if(fd is worker_thread){
sock, resp = worker->pop_result();
sock.send(resp);
}
if(fd is client_socket){
req = fd.read();
worker->add_task(fd, req);
}
}
}
工作线程中的用法:
fd, req = queue->pop_task();
resp = proc(req);
queue->add_result(fd, resp);