这是一个非常常见的问题,尤其是在您开发网络服务器端程序时。大多数 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 毫秒)。
如果 proc() 会运行很长时间,我们需要多线程,并在单独的线程(工作线程)中执行 proc()。
我们可以在不阻塞主线程的情况下向工作线程提交任务,使用基于互斥锁的消息队列,速度足够快。
epoll_add(serv_sock);
while(1){
ret = epoll_wait();
foreach(ret as fd){
req = fd.read();
queue.add_job(req); // fast, non blockable
}
}
然后我们需要一种从工作线程中获取任务结果的方法。如何?如果我们只是直接检查消息队列,在 epoll_wait() 之前或之后。
epoll_add(serv_sock);
while(1){
ret = epoll_wait(); // may blocks for 10ms
resp = queue.check_result(); // fast, non blockable
foreach(ret as fd){
req = fd.read();
queue.add_job(req); // fast, non blockable
}
}
但是,检查动作将在 epoll_wait() 结束后执行,如果 epoll_wait() 等待的所有文件描述符都未处于活动状态,通常会阻塞 10 微秒(常见情况)。
对于服务器来说,10 毫秒是相当长的时间!当任务结果生成时,我们可以用信号 epoll_wait() 立即结束吗?
是的!我将在我的一个开源项目中描述它是如何完成的:
为所有工作线程创建一个管道,并且 epoll 也在该管道上等待。一旦生成任务结果,工作线程将一个字节写入管道,然后 epoll_wait() 将几乎同时结束!- Linux 管道有 5 us 到 20 us 的延迟。
在我的项目SSDB(与 Redis 协议兼容的磁盘内 NoSQL 数据库)中,我创建了一个 SelectableQueue 用于在主线程和工作线程之间传递消息。就像它的名字一样,SelectableQueue 有一个文件描述符,可以通过 epoll 等待。
可选队列: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 queue){
sock, resp = queue->pop_result();
sock.send(resp);
}
if(fd is client_socket){
req = fd.read();
queue->add_task(fd, req);
}
}
}
工作线程中的用法:
fd, req = queue->pop_task();
resp = proc(req);
queue->add_result(fd, resp);