LeaderFollower模式是一种高效的多线程IO多路分离和调度模式。
http://www.kircher-schwanninger.de/michael/publications/lf.pdf
实现高性能的多线程应用程序是具有挑战性的任务,Leader Follower主要解决了以下几个方面的问题:
1. 有效复用IO句柄和线程: 实现高性能多线应用需要并发处理大量的事件,例如连接事件,读写事件,计时器事件等,这些通常都是发生在IO句柄上的事件。因此一个设计上的关键挑战是确定IO句柄和线程复用之间的关联。对于服务器应用程序来说,每个线程只关联一个IO句柄在大多数情况下是不可行的,因为这样的设计无法随着句柄数量的增加有效的扩展。因此,使用少量的线程来从大量IO句柄中分离事件应该是必要的。 相反,对客户端应用程序来说,可能有大量的线程与同一个服务器进行通信。在这种情况下,每连接一线程可能会消耗大量操作系统的资源。因此,复用客户线程产生的事件到少量的连接中也是必要的。例如,一个客户端到服务器只维护一条用于通信的连接。
2. 最小化并发相关的开销: 为了最大限度地提高性能,必须要减少由于并发导致的开销,例如上下文切换,同步,缓存一致性管理。特别是需要每次请求都需要动态分配内存,并且需要跨越多个进程的并发模式在传统的多处理器操作系统下会产生严重的开销。
3. 避免竞争条件: 多个线程在处理大量IO句柄分离事件时,必须要相互协作以防止竞争条件。竞争条件可能发生在多个线程同时尝试访问或更改确定类型的IO句柄。这个问题可以通过互斥锁,信号量,条件变量等同步器来避免。例如,一组线程不能同时对一组socket句柄调用select来分离事件,因为操作系统会错误的将句柄状态返回给每个线程,这些线程需要重新同步以避免多个线程从同一个句柄中读取数据。
同一时间只允许一个线程(leader)等待事件源产生的事件,与此同时,其他线程(follower)可以等待成为leader。
模式中的每个线程都是对等的并在以下三种状态之间转化:
- Leader 最多只有一个线程处于此状态,等待事件(通常是accept事件)发生,然后处理事件或将事件分发给Follower,处理或分发完毕后转化为processing状态。
- Processing 状态与其他Processing线程以及Leader线程并行执行,通常执行完毕后会转化为follower状态,当然如果当前没有leader也可以立即转化为leader状态。
- Follower 空闲状态,等待成为leader或processing。当前leader退出时,可能晋升为新的leader,当接收到leader分发的事件时,转化为processing状态。
阅读过的开源软件中Nginx,Cherokee以及boost.asio.io_service的实现中都采用了类似于Leader Follower的实现。
其中Nginx采用的是accept锁来实现进程之间accept事件(事件源)的协同,只有尝试获取accept锁成功的进程才可以等待accept事件的发生(Nginx通过将listen描述符加入到当前进程的事件反映其中),否则只能处理现有的事件。同一进程内的线程采用的是半同步半异步的模式,即主线程等待事件的发生,然后通过队列将事件分发给处理线程。Nginx的实现中是没有follower状态的。
Cherokee采用的是accept加锁实现线程之间事件源的协同,当当前线程没有事件可供处理时,阻塞获取accept锁(follower状态),否则非阻塞获取accept锁,并以非阻塞模式查看是否有就绪的listening事件,如果有listening事件(leader状态)直接accept获取新的事件,将事件添加到自身处理列表,并释放锁,开始处理现有的事件(processing)。
boost.asio.io_service中采用的是leader处理reactor事件,并reactor会产生新的事件并将分发到一个就绪队列中,并将reactor事件push到就绪队列的末尾(这样当再次处理完所有事件时,reactor又会被调用,也就是说就绪队列中始终存在一个reactor事件),然后通过信号量唤醒follower状态的线程,将自身切换为processing。processing状态的线程从队列中获取事件并处理,当没有事件可供处理时,进入follower状态等待唤醒。boost.asio.io_service中虽然任意一个事件可能在任何一个线程上下文中运行,上下文的切换似乎不可避免,但是boost.asio提供了strand类,用于限制每个事件所在的线程上下文,因此上下文切换是可以避免的。
Nginx和Cherokee中都将listening事件作为其他事件的事件源,这是因为他们处理的都是http服务,事件源只有外部连过来的连接,这样每一个描述符都在特定的线程或进程中,可以最小化上下文切换。而boost.asio需要考虑他的通用性,因此将reactor作为事件源。