一说起Leader/Followers并发模式,都会与Half-Async/Half-Sync并发模式进行比较,说LF模式更加高性能,成了一个高性能名词标签
符号,相反HA/HS仿佛成了一个低性能的名词标签,如果你的线程池不使用LF模式就谈论不上高效,要是你还在使用HA/HS模式,马上笼
统地建议换成LF模式,一切的问题会归根于HA/HS模式。那么为什么LF模式没有成一个标准的默认的并发模式呢,彻底取代其它并发模
式呢。因为Leader/Followers并发模式是设计用来解决Half-Async/Half-Sync并发模式在特定的使用场合中遇到的问题的。当使用
Half-Async/Half-Sync并发模式的变种Half-Reactive/Half-Sync的时,会伴随以下问题。
Half-Reactive/Half-Sync即,由一个线程进行Reactor(反应器)进行同步事件分离程序以及同步事件处理分派程序。事件处理程序可
以通过线程池共享队列,交给可以进行同步操作的线程进行。同步IO在Reactive线程中执行,事件后续处理交给Sync线程(s)。在Web服
务器,Reactive线程阻塞等待请求(数据接收)事件,并不阻塞地同步接收请求,将会因为流控而阻塞或大负载的文件输送操作移交到
Sync线程池的某一线程进行。
问题一:
这样的实现版本中就只有Reactive线程一个在进行同步I/O操作,虽然有多个句柄的同步指示事件通知不再阻塞,但却不能充分发挥多
线程将这些不再阻塞的句柄并发同步I/O操作。如果将同步I/O读取操作也移交Sync线程池的话,Reactive线程必须同步这些并发,等待
这些并发完成后再进行新一轮的同步事件分离程序等待。
问题二:
请求要输送到其它线程,所以必须使用堆复制请求。由于在多处理器环境中,线程的切换往往意味着处理器的切换,而切换处理器就会
有内存同质的问题,或者缓存(L1,L2 cache或MMU快表TLB)重新装载(即不亲和coherency一致)。简单地可以想像在内存寻址时,
CPU缓存缓存了内存页表及内存页表项,以便不用访问内存中的页表和页表项进行二次或多次内存访问。如果同步IO读出请求可以在同
一线程上处理,就可以尽量避免处理器的切换。但是这时又引入了另一个问题。要是所有线程都可以进行Reactor等待事件就好了。
问题三:
Reactor使用的同步事件分离程序,如select等往往是不可以对相同的句柄在多个线程进行阻塞等待的。如何能将同步事件通知不再阻
塞的句柄上的同步I/O并发掉,又可以保证只有一个线程使用Reactor进行同步事件分离程序等待,那么Leader/Followers模式就是最适
合且自然的选择了。
Leader/Followers并发模式是设计用来解决Reactor(反应器)在实际应用中使用线程池进行事件处理而伴随的问题。在基于IOCP的
Proactor(前摄器)应用多线程的场合,Leader/Followers并发模式就并不必要同时也不是最合适的选择,Ice的ThreadPool就是一个
实例,尽管在其文档上标榜其使了LF模式。
一次请求事件处理中包含了两步操作:同步不阻塞I/O读出请求以及处理请求。HA/HS模式,将上面两步归为异步不阻塞和同步,将一次
请求事件处理分别在两层服务的线程上执行。而LF模式是想方设法让一次请求事件处理执行在同一线程内。但是在Proactor事件模式中
,Proactor分离的是完成事件,随后的事件处理并不包含同步不阻塞I/O读出请求,而是直接对请求进行处理。并且IOCP可以被多个线
程进行事件分离等待,而IOCP本质是一个完成事件队列,事件处理的线程其实在共享一个队列,将完成事件同步处理,而系统相当于异
步服务层执行了我们发起的异步IO操作,并将完成异步IO操作的完成事件放入到IOCP的完成事件队列。Proactor模式可以视作HA/HS模
式的扩展,这时并不需要使用LF模式。而Half-Sync/Half-Reactiver的中层队列层就相当于一个I/O的完成事件队列,所以在基于IOCP
的Proactor上使用LF模式线程就不必要了,因为这里没有LF模式所要解决的问题。
Half-Async/Half-Sync并发模式是一种并发关系的模式,并不等同于多线程或线程池。系统的socket协议栈和我们在应用程序中使用的
socket同步操作接口,应用了这一并发模式。系统的协议栈在进行网卡的硬中断处理程序时,不可阻塞或长时间执行,必须高效地完成
以网卡缓存的数据交换和控制。这就相当于Half-Async层。系统离开系统调用前进行软中断处理程序将数据在协议栈向上传递到传输层
的缓存队列中,从而唤醒我们在应用发起的同步I/O操作。
Half-Async/Half-Sync并发模式应用在线程池设计时,就分为三个层,Half-Async不阻塞服务层,Half-Sync允许阻塞服务层,中间由
一层队列服务层进行交互。通常有一个线程池进行Half-Sync的服务。但并不是说Half-Async就只能一个线程进行服务,如果服务于
Half-Async层的线程不用进行同步并发的话,一样可以使用多个线程并发于Half-Async层,原则只有一个不可以阻塞。Half-
Reactive/Half-Sync模式的线程池中只有一个Reactive线程,是因为Reactor(反应器)使用同步事件分离程序不可以被多个线程对相
同的句柄进行等待。
其实epoll使用了类似Proactor模式,但是它发起的不是异步IO,而是异步的事件轮询。虽然epoll称自己是异步事件,但它的
epoll_wait事件分离程序却是句柄的同步事件。epoll利用句柄的等待队列,插入一个等候并绑定与poll不一样的处理程序,处理程序
将有事件通知的句柄入队到就绪队列。epoll(epoll_ctl而不是epoll_wait)发起的是异步的轮询,而就绪队列发生的是异步轮询的完
成事件,这个完成事件中包含了句柄的同步事件通知。epoll_ctl已经向句柄的发起了异步轮询,epoll_wait则是等待在就绪队列上分
离轮询完成的事件。为什么不干脆将事件处理程序挂在句柄的等待队列上,因为等待队列的处理程序运行在内核态,回调往往意味着颠
倒控制,在内核态回调用户态的代码,对于系统来说这是一件愚蠢的事情。
在《ACE程序员指南》一书中,作者为了突出LF模式比HA/HS模式的优越,给了一个HA/HS模式的实例。在ACE中,线程类继承ACE_Task可
以同时获得独立的消息(泵)队列服务。位于HA层为Manager线程类,维护着HS层的Worker线程类。HA层与HS层之间的队列服务层并不
由共享的队列方式实现,而是使用Worker线程类的独立消息(泵)队列来充当。这样的结果是,Worker线程类阻塞在各自的消息队列上
,事件处理必须经由HA层的Manager选出一个Worker并向它的消息队列发送消息。Worker线程被唤醒后处理事件,并在完成后入队到
Manager的空闲线程队列中。而Manager在它的消息队列上收到消息后,只是选出空闲Worker并转送消息。在他的这个模式实例中,每个
消息都必须唤醒Manager线程,再由Manager线程选出Worker线程并唤醒处理消息。即每次消息都唤醒两个线程,增加上下文切换来突出
LF模式的优越。同时Manager在选出Worker线程的执行,和Worker线程重新入队Manager的空闲队列又增加了线程同步操作。
ACE框架中有几个版本的Reactor,也只为基于select的Reactor提供了ThreadPool(LF模式)的版本ACE_TP_Reactor(,还有
Dev_Poll_Reactor,用于linux的epoll以及solaris的/dev/poll),而基于Windows系统的WaitForMutilpleObjects版本的
ACE_WFMO_Reactor,并没有提供一个LF线程池,因为WFMO可以在多个线程上同时对相同的内核对象进行事件等待,这是select所不能做
到的。另外ACE_Proactor也就没有提供LF线程池。ACE框架最终还是将线程池和线程池的并发模式的决定权交给了框架的使用者,除了
ACE_TP_Reactor,因为这是一个经典的案例,利用LF模式解决基于select的Reactor使用多线程事件处理时遇到的问题。
在Ice项目中,ThreadPool的Reactor版本实现了LF模式,线程池使用了Reactor进行事件分离。而在ACE项目中,由基于*nix的事件分离
程序的Reactor的事件处理循环实现LF并发模式,但并不提供线程池,多线程只需要运行Reactor的事件处理循环。这是不一样的出发点
的设计。