◆◆◆
大多数事件驱动的软件使用多线程来并行地处理多个事件。然而,要想以高效的、可预见的和简单的方式为线程分配工作却出奇的困难。
在事件驱动软件中,尤其是服务器软件中,经常需要在线程和事件源之间定义高效的事件分离机制。同时,在使用多线程分离一系列共享事件源上的事件的时候,还需要防止出现竞争状态。例如,一个Web服务器可能在多个I/O句柄上使用多个线程服务于多个GET请求,以保证其可扩展性。有些将线程和事件源关联起来的方法效率太低,而且需要付出高昂的代价。例如,为每个请求创建一个线程,或者为每个事件源安排一个单独的线程,由于操作系统和硬件的扩展性限制,这些方式的效率都非常低。所以,我们需要一个即容易实现、效率又高的并发响应软件架构。
因此:
使用一个预分配的线程池来协调事件的检测、分离、分发和处理。 在这个线程池中,一次只有一个线程——Leader(领导者)——等待一系列共享的事件源上的事件。 当事件到来时,Leader将池中另一个线程提升为新的Leader,然后自己与其他处理线程以并行的方式进行事件处理。
在Leader监听事件源等待事件出现的同时,其它线程(即Follower,跟随者)处于排队和休眠状态,直到被提升为Leader。如果当前的Leader线程检测到事件源中的一个事件,它会做两件事情:首先,它把一个Follower线程提升为一个新的Leader,然后它自己将变为一个加工者(processor)线程将事件分离并分派给指定的事件处理程序,事件处理程序运行在接收事件的线程中。在当前的Leader线程等待共享事件源上发生新的事件的同时,多个处理线程可以并行地处理事件。处理完事件,处理线程还原为Follower角色,并且保持休眠状态直到再次被提升为Leader。
◆◆◆
通过预先分配一个线程池,LEADER/FOLLOWERS设计避免了动态线程创建和销毁的额外开销。将线程放在一个自组织的池中,而且无需交换数据,这种方式将上下文切换、同步、数据移动和动态内存管理的开销都降到了最低。而且,让Leader线程执行对下一个Follower的提升可以避免由于存在一个中心提升决策者而带来的性能瓶颈。
而为这些性能优化所付出的代价是其受限的适用范围。LEADER/FOLLOWERS仅在处理短暂的、原子的、反复的和基于事件的动作上取得了成功,比如接收和分发网络事件或者向数据库存储大量数据记录。事件处理程序所提供的服务越多,其体积也就越大,而处理一个请求所需的时间越长,池中的线程占用的资源也就越多,同时也需要更多的线程。相应的,应用程序中其它功能可用的资源也就越少,从而影响到应用程序的总体性能、吞吐量、可扩展性和可用性。
在大多数LEADER/FOLLOWERS设计中共享的事件源封装在一个分配器组件中。如果在一个设计中联合使用了LEADER/FOLLOWERS和REACTOR事件处理基础设施,其响应(reactor)组件便是分配器。封装事件源将事件分离和分派机制与事件处理程序隔离开来。如果正在提升新的Leader线程,而对最近的事件的处理恰好同时完成就可能出现竞争状态,为分配器提供停用和重激活特定事件源的方法,可以避免这种情况。
线程池可以用RESOURCE POOL(503)来实现;而事件的分配和对共享事件源的同步访问则可以用Monitor Object(368)来实现。该设计通过使用自组织的并发模型来提高性能,该模型可以避免在事件源和事件处理程序中间引入单独的查询层的开销。
在线程池中,Monitor Object为线程提供两个方法。一个是join(加入)方法,使用这个方法可以把新初始化的线程加入到池中。新加入的线程将自己的执行挂起到线程池监听者条件(monitor condition)上,并开始等待被提升为新的Leader。在它变成一个Leader之后,它便可以访问共享的事件源,等待执行下一个到来的事件。另一个是promote_new_leader方法,当前的Leader线程使用这个方法可以提升新的Leader,其做法是通过线程池监听者条件通知休眠的Follower。收到通知的Follower继续执行(resume)线程池的join方法,访问共享事件源,并等待下一个事件的到来。
我们还可以通过使用TEMPLATE METHODS(453)和STRATEGIES(455)来支持多种提升协议,比如后进先出、先进先出和最高优先级等。
15.3 Active Object **
在开发ENCAPSULATED IMPLEMENTATION(313),或者HALF-SYNC/HALF-ASYNC(359)架构中的同步服务层,或者ACCEPTOR-CONNECTOR(265)配置中的服务处理器时…
…我们通常需要确保组件的操作可以在它们自己的控制线程中并行的运行。