6.6.2 常见的并发网络服务程序设计方案(6)
线程池的另外一个作用是执行阻塞操作。比如有的数据库的客户端只提供同步访问,那么可以把数据库查询放到线程池中,可以避免阻塞IO 线程,不会影响其他客户连接,就像Java Servlet 2.x 的做法一样。另外也可以用线程池来调用一些阻塞的IO 函数,例如fsync(2)/fdatasync(2),这两个函数没有非阻塞的版本30。
如果IO 的压力比较大,一个Reactor 处理不过来,可以试试方案9,它采用多个Reactor 来分担负载。
方案9 这是muduo 内置的多线程方案,也是Netty 内置的多线程方案。这种方案的特点是one loop per thread,有一个main Reactor 负责accept(2) 连接,然后把连接挂在某个sub Reactor 中(muduo 采用round-robin 的方式来选择sub Reactor),这样该连接的所有操作都在那个sub Reactor 所处的线程中完成。多个连接可能被分派到多个线程中,以充分利用CPU。
muduo 采用的是固定大小的Reactor pool,池子的大小通常根据CPU 数目确定,也就是说线程数是固定的,这样程序的总体处理能力不会随连接数增加而下降。另外,由于一个连接完全由一个线程管理,那么请求的顺序性有保证,突发请求也不会占满全部8 个核(如果需要优化突发请求,可以考虑方案11)。这种方案把IO 分派给多个线程,防止出现一个Reactor 的处理能力饱和。
与方案8 的线程池相比,方案9 减少了进出thread pool 的两次上下文切换,在把多个连接分散到多个Reactor 线程之后,小规模计算可以在当前IO 线程完成并发回结果,从而降低响应的延迟。我认为这是一个适应性很强的多线程IO 模型,因此把它作为muduo 的默认线程模型(见图6-13)。
|
| (点击查看大图)图6-13 |
方案9 代码见:examples/sudoku/server_multiloop.cc。它与server_basic.cc 的区别很小,最关键的只有一行代码:server_.setThreadNum(numThreads);- $ diff server_basic.cc server_multiloop.cc -up
- --- server_basic.cc 2011-06-15 13:40:59.000000000 +0800
- +++ server_multiloop.cc 2011-06-15 13:39:53.000000000 +0800
- @@ -21,19 +21,22 @@ class SudokuServer
- - SudokuServer(EventLoop* loop, const InetAddress& listenAddr)
- + SudokuServer(EventLoop* loop, const InetAddress& listenAddr, int numThreads)
- : loop_(loop),
- server_(loop, listenAddr, "SudokuServer"),
- startTime_(Timestamp::now())
- {
- server_.setConnectionCallback(
- boost::bind(&SudokuServer::onConnection, this, _1));
- server_.setMessageCallback(
- boost::bind(&SudokuServer::onMessage, this, _1, _2, _3));
- + server_.setThreadNum(numThreads);
- }
方案10 这是Nginx 的内置方案。如果连接之间无交互,这种方案也是很好的选择。工作进程之间相互独立,可以热升级。
方案11 把方案8 和方案9 混合,既使用多个Reactor 来处理IO,又使用线程池来处理计算。这种方案适合既有突发IO (利用多线程处理多个连接上的IO),又有突发计算的应用(利用线程池把一个连接上的计算任务分配给多个线程去做),见图6-14。
|
| (点击查看大图)图6-14 |