6.6.2 常见的并发网络服务程序设计方案(7)
这种方案看起来复杂,其实写起来很简单,只要把方案8 的代码加一行server_.setThreadNum(numThreads); 就行,这里就不举例了。
一个程序到底是使用一个event loop 还是使用多个event loops 呢?ZeroMQ 的手册给出的建议是31,按照每千兆比特每秒的吞吐量配一个event loop 的比例来设置event loop 的数目,即muduo::TcpServer::setThreadNum() 的参数。依据这条经验规则,在编写运行于千兆以太网上的网络程序时,用一个event loop 就足以应付网络IO。如果程序本身没有多少计算量,而主要瓶颈在网络带宽,那么可以按这条规则来办,只用一个event loop。另一方面,如果程序的IO 带宽较小,计算量较大,而且对延迟不敏感,那么可以把计算放到thread pool 中,也可以只用一个event loop。
值得指出的是,以上假定了TCP 连接是同质的,没有优先级之分,我们看重的是服务程序的总吞吐量。但是如果TCP 连接有优先级之分,那么单个event loop 可能不适合,正确的做法是把高优先级的连接用单独的event loop 来处理。
在muduo 中,属于同一个event loop 的连接之间没有事件优先级的差别。我这么设计的原因是为了防止优先级反转。比方说一个服务程序有10 个心跳连接,有10 个数据请求连接,都归属同一个event loop,我们认为心跳连接有较高的优先级,心跳连接上的事件应该优先处理。但是由于事件循环的特性,如果数据请求连接上的数据先于心跳连接到达(早到1ms),那么这个event loop 就会调用相应的eventhandler 去处理数据请求,而在下一次epoll_wait() 的时候再来处理心跳事件。因此在同一个event loop 中区分连接的优先级并不能达到预想的效果。我们应该用单独的event loop 来管理心跳连接,这样就能避免数据连接上的事件阻塞了心跳事件,因为它们分属不同的线程。
结语
我在§3.3 曾写道:
总结起来, 我推荐的C++(www.cppentry.com) 多线程服务端编程(www.cppentry.com)模式为:one loop per thread +thread pool。
event loop 用作non-blocking IO 和定时器。
thread pool 用来做计算,具体可以是任务队列或生产者消费者队列。
当时(2010 年2 月)写这篇博客时我还说:“以这种方式写服务器程序,需要一个优质的基于Reactor 模式的网络库来支撑,我只用过in-house 的产品,无从比较并推荐市面上常见的C++(www.cppentry.com) 网络库,抱歉。”
现在有了muduo 网络库,我终于能够用具体的代码示例把自己的思想完整地表达出来了。归纳一下32 ,实用的方案有5 种,muduo 直接支持后4 种,见表6-2。
表6-2
表6-2 中的N 表示并发连接数目,C1 和C2 是与连接数无关、与CPU 数目有关的常数。
我再用银行柜台办理业务为比喻,简述各种模型的特点。银行有旋转门,办理业务的客户人员从旋转门进出(IO);银行也有柜台,客户在柜台办理业务(计算)。要想办理业务,客户要先通过旋转门进入银行;办理完之后,客户要再次通过旋转门离开银行。一个客户可以办理多次业务,每次都必须从旋转门进出(TCP 长连接)。另外,旋转门一次只允许一个客户通过(无论进出),因为read()/write() 只能同时调用其中一个。
方案5:这间小银行有一个旋转门、一个柜台,每次只允许一名客户办理业务。而且当有人在办理业务时,旋转门是锁住的(计算和IO 在同一线程)。为了维持工作效率,银行要求客户应该尽快办理业务,最好不要在取款的时候打电话去问家里人密码,也不要在通过旋转门的时候停下来系鞋带,这都会阻塞其他堵在门外的客户。如果客户很少,这是很经济且高效的方案;但是如果场地较大(多核),则这种布局就浪费了不少资源,只能并发(concurrent)不能并行(parallel)。如果确实一次办不完,应该离开柜台,到门外等着,等银行通知再来继续办理(分阶段回调)。
方案8:这间银行有一个旋转门,一个或多个柜台。银行进门之后有一个队列,客户在这里排队到柜台(线程池)办理业务。即在单线程Reactor 后面接了一个线程池用于计算,可以利用多核。旋转门基本是不锁的,随时都可以进出。但是排队会消耗一点时间,相比之下,方案5 中客户一进门就能立刻办理业务。另外一种做法是线程池里的每个线程有自己的任务队列,而不是整个线程池共用一个任务队列。这样的好处是避免全局队列的锁争用,坏处是计算资源有可能分配不平均,降低并行度。
方案9:这间大银行相当于包含方案5 中的多家小银行,每个客户进大门的时候就被固定分配到某一间小银行中,他的业务只能由这间小银行办理,他每次都要进出小银行的旋转门。但总体来看,大银行可以同时服务多个客户。这时同样要求办理业务时不能空等(阻塞),否则会影响分到同一间小银行的其他客户。而且必要的时候可以为VIP 客户单独开一间或几间小银行,优先办理VIP 业务。这跟方案5 不同,当普通客户在办理业务的时候,VIP 客户也只能在门外等着(见图6-11 的右图)。这是一种适应性很强的方案,也是muduo 原生的多线程IO 模型。
方案11:这间大银行有多个旋转门,多个柜台。旋转门和柜台之间没有一一对应关系,客户进大门的时候就被固定分配到某一旋转门中(奇怪的安排,易于实现线程安全的IO,见§4.6),进入旋转门之后,有一个队列,客户在此排队到柜台办理业务。这种方案的资源利用率可能比方案9 更高,一个客户不会被同一小银行的其他客户阻塞,但延迟也比方案9 略大。