我应该使用epoll还是在线程中使用阻塞recv?

9 投票
2 回答
2915 浏览
提问于 2025-04-17 01:52

我正在尝试写一个可扩展的自定义网络服务器。现在我有了一些进展:

主循环和请求解析器是用Cython写的。主循环负责接受连接,并把这些连接的套接字分配给池中的某个进程(必须使用进程,因为线程在多核硬件上不会有好处,这是因为有个叫GIL的东西)。

每个进程都有一个线程池。进程会把套接字分配给一个线程。这个线程会在套接字上调用recv(这是一个阻塞操作),然后等待数据到来。当数据到达时,它会被传送到请求解析器,然后通过WSGI发送给在该线程中运行的应用程序。

现在我听说过epoll,但有点困惑。使用epoll来获取套接字数据,然后直接传递给进程有什么好处吗?还是说我应该继续让每个线程在recv上等待呢?

顺便问一下,epoll到底是用来干什么的?看起来多线程和阻塞的fd调用也能实现同样的效果。

2 个回答

1

epoll 函数(还有其他类似的 pollselect 函数)可以让你写出单线程的网络代码,来管理多个网络连接。因为没有使用多线程,所以不需要像多线程程序那样处理同步问题(这在多线程中有时会很复杂)。

另一方面,你需要为每个连接明确地定义一个状态机。在多线程程序中,这个状态机是隐含的。

这些函数提供了一种在一个进程中处理多个连接的方式。有时候不使用线程会更简单,而有时候你已经在使用线程,这时候使用阻塞套接字会更方便(在Python中,这样可以释放全局解释器锁)。

11

如果你已经在使用多个线程,那么epoll对你来说并没有太大的额外好处。

epoll的主要作用是让一个线程可以同时监听多个文件选择器的活动(并在每次发生事件时做出反应),这样就能实现事件驱动的多任务处理,而不需要额外创建更多的线程。虽然线程的开销相对较小(比起创建进程来说),但每个线程还是需要一些资源(毕竟每个线程都要维护自己的调用栈)。

如果你愿意,可以把你的进程池改写成单线程的,使用epoll,这样可以减少你使用的线程总数,但你得考虑这是否对你来说重要——一般来说,如果每个工作线程同时处理的请求数量不多,创建线程的开销就不算太大;但如果你希望每个工作线程能处理成千上万的连接,那这个开销就会变得很重要(这就是epoll的优势所在)。

但是……

你描述的内容听起来有点像是在重复造轮子——你的:

  1. 主循环和请求解释器
  2. 进程池

听起来几乎和:

  1. nginx(或者其他负载均衡器/反向代理)
  2. 一个预先分叉的tornado应用

完全一样。Tornado是一个单线程的Web服务器Python模块,使用epoll,并且内置了预分叉的功能(这意味着它会创建多个自己的副本作为独立进程,从而形成一个进程池)。Tornado是基于为Friendfeed开发的技术——他们需要一种方法来处理大量的长连接,以便为寻找新实时更新的客户端提供服务。

如果你这样做是为了学习,那就尽管去“重复造轮子”吧!这是一个很好的学习方式。但如果你真的是想在这些基础上构建一个应用,我强烈建议你考虑使用现有的、稳定的、社区开发的项目——这会为你节省很多时间,避免很多不必要的麻烦和潜在的问题。


(顺便说一句,我喜欢你的头像。<3)

撰写回答