分享

Leader/Follower 多线程模式

 WUCANADA 2012-03-27
Leader/Follower 多线程模式
2010年09月26日 星期日 下午 6:13


昨天找来了原始的pdf文档,LF.pdf. 硬着头皮读了几章,有些地方理解不能,关键是英文太菜了,句子一长就不知道说什么了。 还是勉强翻译了一下,还没完,15页呢,只看了一半,似懂非懂。贴上来共享下。

翻译的很烂,有些词不知道怎么表达,别骂我,多谢。
PS: TMD摆渡总提示我有不合适的词,瞪着眼找了半天居然是"插 入"两个字...杯具啊!


Leader/Followers 一个高效的多线程IO多路分离和分发设计模式
翻译: 英文超烂的sodarfish

概要
LF设计模式提供了一种并发模型,它允许多线程高效地进行事件的多路分离和分派事件处理过程,以处理多个线程共享的IO句柄。
示例
考虑设计一个多层次的、大容量的、实时在线交易处理系统,如下图,前置通信服务器将来自客户端的交易请求转发到后端数据库服务器进行处理,客户端可以是如旅行社、索赔处理中心或者销售终端等。该多层模型已经通过负载均衡和冗余的方式来提升整个系统的吞吐量和可靠性。
前置通信服务器是个名副其实的”混血儿”,它主要负责两项任务。首先,它处理来自成千上万远程客户端通过WAN同时发来的请求;其次,它检测请求的有效 性,并将有效请求通过TCP/IP链接发送到后端数据库服务器。相对来说,数据库服务器是一个只处理特定交易的”纯粹的”服务器. 在交易完成之后,数据库服务器将结果返回给通信服务器,进而返回给远程客户端。

改进OLTP性能的通常策略是使用多线程同时处理多个请求。 理论上每个线程可以独立地运行,通过复用OLTP计算相关的网络和磁盘IO处理,如校验、索引检索、表合并,存储过程等,以增加整个系统的吞吐量。 但实际上,设计一个令前端和后端进行高效IO操作和OLTP处理的多线程模型并不容易。

一种方式是使用基于半同步/半反应(半同步半异步模型的变体)模式的线程池. 在大规模OLTP系统中,IO链接数往往比线程数大很多,在这种情况下,像select, poll, waitForMultipleObjects 等事件多路调节器被用于等待一个socket集合上发生的事件. 一些特定的多路分离器,例如著名的select, poll, 如果被多个线程在同一个socket集上调用,将会发生问题。 为了突破这个限制,可以给多路分离器设计一个专有的线程(IO线程),如下图所示。当集合中有句柄发生活动时,事件多路分离器返回控制器给IO线程,并指 定哪些句柄即将发生事件。 IO线程从特定的socket句柄中读取交易请求,将其存储到一个动态分配的命令对象,并插入到一个消息队列中,该队列以监视对象模式实现。 该队列由工作线程池提供服务,当一个工作线程可用时,它从队列中移出一个命令对象,处理指定的交易,然后给通信服务器一个响应。

类似的模式也可以应用到前置通信服务器,单独的IO线程和工作线程池用于校验和转发请求到后端服务器。在这个设计中,前置通信服务器依然扮演后端服务器的 客户端角色,因此,前置机要等待后端返回交易结果。前置机收到结果后,结果必须分配给适当的工作线程。此外,在多层系统中,前端可能在处理前端请求的同时 对后端返回的结果做出响应。因此前置机必须总能够在处理请求和发送响应,这意味着前置机的工作线程在同一时间不能全部在阻塞状态。

尽管上面的模型被用于许多的并发应用程序,但是在大容量系统中,例如我们的OLTP例子,它会带来过度的负荷。例如,即使是很轻的工作量,半同步/半反应 线程池的设计也会导致动态内存分配、多操作同步以及上下文切换,以在工作线程和IO线程之间传递命令对象,这会使最佳响应时间不必要地变长。 此外,如果OLTP服务器运行在多处理器环境下,处理器的缓存一致性要求在多个线程之间传递命令对象,这会导致高负荷情况发生。
如果OLTP服务运行在一个支持异步IO的操作系统上,那么半同步半反应模式的线程池可以被替换为基于Preactor模式的纯粹的异步线程池,由于去掉 了IO线程,在一定程度上减少了负荷。 许多操作系统不支持异步io,而且即使实现了,也不见得高效。(貌似现在好多了哈),目前,多线程对于OLTP服务来说是非常必要的。

背景(上下文)
在应用中,发生在IO句柄集合上的事件必须被分离并高效的分配到多个线程。

问题
多线程是用于实现并行处理多IO事件的通用技术. 实现一个高性能的多线程应用并不简单,无论如何,要有效的应对这个问题,下面的几条必须要满足:
高效的分离IO句柄和线程: 高性能多线程服务器会同时处理多种类型的事件,例如连接、读、写事件。这些事件通常发生在分配给每个客户端连接的TCP/IP套接字上。关键问题是,确定线程和IO句柄直接的高效的关联方式。
对于服务器程序来说,为每个IO句柄关联一个线程是不现实的,因为此设计可能不会随着IO句柄的增长而有效伸缩。有必要设置一个较小的、固定数量的线程来 复用大量的IO句柄。相反的,一个客户端程序可能有大量的线程连接到同一个服务端,此时每个线程一个连接的方式会消耗过多系统资源。因此,将客户端的大量 线程对少量的连接进行复用是必要的,例如,维护一个单独的从客户端到服务端的连接。
举个例子,如果一个OLTP服务器使用每个线程一个连接的并发模型,将无法处理大量的并发请求。此时使用多路复用模型,使用线程池来令线程数与系统可用资源保持一致,例如CPU数量,而不是令线程数与连接数一致。
同样地,为了保存系统资源,前端中的每个线程复用同一个连接,如图所示。这样,当前端收到由后端返回的结果时,必须分离出结果并交给适当的线程去处理。

减少同步相关的消耗
要将性能最优化,同步相关消耗的几个关键来源:上下文切换、同步、缓存一致性管理等必须最小化。 实际上,要求动态的分配内存给每个请求,并在多个线程之间传递将带来大量的消耗(在传统多处理器操作系统上)。
举例,我没的OLTP例子使用了基于半同步/半反应的并发模型。它利用一个消息队列来将负责从客户端取请求事件的网络IO线程解耦, 在工作线程池中,工作线程处理这些事件并返回给客户端。

不幸的是,这个设计要求内存被动态的分配,从IO线程的堆内存或者公共内存区,这样请求事件才能被加入到消息队列。另外,它要求若干个同步和上下文切换来完成在消息队列上的加入和移除动作。


防止竞争:
多线程在同一个IO集合上进行事件分离,必须协同以防止竞争条件产生。竞争会出现在多个线程同时访问同种类型的IO句柄时。这可以通过同步解决,例如互斥体,信号了或者条件变量。
例如,线程池可以用select 来分离一个socket集合,当IO事件在同一个句柄集合上准备就绪时,操作系统会错误地通知到多个调用select的线程。此时,线程池需要进行同步, 以防止多个线程同时读同一个socket句柄,这会导致数据丢失。同样地,多个线程同时写socket句柄,会导致数据混乱。

解决方案
领倒者/跟随者模式包括如下几个部分:
句柄和句柄集合: 句柄是对IO资源的标识,例如一个socket连接或打开的文件, 通常由操作系统实现和管理。句柄集合由多个IO句柄组成,用于监听该集合中包含的句柄上发生的事件。当集合中某个句柄上可能发起一个非阻塞操作时,句柄集合将被返回给它的调用者。
例如,OLTP服务器关心两种事件类型—连接事件 和 读事件 –分别代表了接入连接 和 交易请求。 前端和后端程序对每个客户端维护单独的一条连接。每个连接在服务器上表现为一个单独的socket句柄。 我们的OLTP服务器使用select 多路分离器来确定哪些句柄有即将到来的事件,对句柄进行IO操作而不阻塞它的调用线程。然而由于IO事件会通知到多个线程,因此不能让多线程在同一个集合 上同时调用select。

事件响应者(event handler): 事件响应者指定了一个接口,其由一个或多个钩子函数组成,这些函数描述了对事件句柄上发生的事件的各个操作集合。

实际事件响应者: 是事件响应者的具体实现。每个实际事件响应都与应用中的一个句柄绑定。 另外,事件响应者实现了用于处理从句柄上收到的事件的各个钩子函数。

例如,OLTP前端程序的事件响应者收到并校验了客户端的请求,并将请求转发到后端服务器。同样的,后端的事件响应者接受前端发来的请求,读写数据库记录来完成交易,然后将结果返回给前端。

领倒者/跟随者 线程集合 线程在该模式中扮演多个角色。 领倒者线程等待句柄集上发生事件,到时它处理事件并执行某些类型的服务。 当线程处理完事件后,它将进入线程集合进行等待。 0个或者多个跟随者线程将排队等待成为领倒者,或从领倒者线程中接收分配来的事件。 当前领倒者从句柄中取得一个事件时,它将推举一个跟随者线程作为新的领倒者,然后自己处理这个事件。跟随者集合可以被隐式地维护,例如,使用信号量或者条 件变量;也可以显式维护,如使用集合类。 用哪种方式取决于领倒者是否必须显式指定某个特定的线程来处理事件的跟监。
例如,每个OLTP后端服务器都有一个线程池,每个线程等待处理交易请求。任何时候线程们都能在处理请求和发送数据给前端通信服务器。最多一个线程做领倒 者,负责监听新的连接和读事件。 其余的线程都是跟随者,他们在池中等待被推举为新的领倒者,或者接收领倒者发来的事件。

动态
LF模式中各组成部分存在两种协作,其依赖于句柄集合中的IO句柄与线程之间的两种关联:绑定和非绑定。

非绑定的句柄/线程关联:这种情况下,线程和IO句柄之间没有固定的关联,因此,每个线程都可以处理发生在任何句柄上的任何事件。这种方式在多个线程轮流共享句柄集合时常用。
例如,OLTP后端服务器的例子在线程池和由select管理的句柄集之间实现了一个非绑定关联。 实际的事件响应者可以在任何一个线程中执行处理。因此,没有必要来维护线程和句柄之间的关系。这种情形简化了后端程序的开发。

绑定的句柄/线程关联:每个线程都绑定到它自己的IO句柄上,各自处理特定的事件。当一个客户端线程在socket句柄上等待服务器对双向请求的响应时常用此方式。 这种情况下,客户端应用线程希望用一个特定的线程来处理该IO句柄上的事件,通常是发出请求的那个线程。

例如,前端通信程序中的线程转发客户请求到后端进行处理。为了减少系统资源的消耗,前端的工作线程会使用多路复用连接于后端通信。当请求发送后,工作线程 等待结果的返回,这种情况下,维护一个绑定关系简化了前端程序,并减少了交易处理中不必要的上下文管理负荷。(注:即工作线程前端与和后端的连接绑定,长 连接适用,注意多个线程不要写同一个句柄)

上面描述了两种关联,下面是LF模式中发生的各种协作。
1.    领倒者多路分离:  领倒者线程等待句柄集上发生事件。
2.    跟随者推举:根据关联的不同,存在两种情况:
对于非绑定线程/句柄关联,当领倒者线程分离出事件后,他选择一个跟随者线程做新的领倒(根据后面讲到的推举协议)。
对于绑定线程/句柄关联,当领倒者分离出事件后,他检查与该事件关联的句柄,以决定哪个线程负责处理它。如果领倒者发现自己负责该事件,就推举一个跟随者 作为新领倒(使用和非绑定情况下一样的协议)。相反的,如果事件是由其他线程负责,那么领倒者必须将事件移交给预定的跟随者线程。这个跟随者线程随即将自 己从线程集合中注销,并同时处理这个事件。于此同时,当前的领倒者继续等待另一个事件发生。

3.    事件处理: 对于非绑定关联, 前一个领倒者线程在推举完下一个领倒者后开始处理其分离出的事件; 对于绑定关联,或者由前任领倒者处理事件,或者由一个跟随者线程处理领倒移交给他的事件。

4.    重加入到 LF 线程集合:为了多路分离句柄集合上的各个句柄,L/F线程集中必定首先有一个线程加入(重新加入)。这个动作通常发生在一个事件被处理完毕,线程准备处理 另外一个事件时。如果当前没有领倒者,一个线程可以立刻变为领倒。否则,该线程必须作为跟随者等待领倒将其提拔。
下图描述了各个部分之间的协作关系。

在任何时刻,LF模式中的各成员都处于以下三种状态:
领倒者: 当前作为领倒者的线程,其等待事件发生,当收到事件时,转为处理中状态。
处理中: 此状态的线程可以与领倒者、跟随者或其他线程同时执行,通常它会转为跟随者状态,如果当他处理完事件时没有领倒者,也可能立刻变为领倒者状态。
跟随者:     此状态的线程在线程池中处于等待状态,当被当前领倒推举时可以转为领倒者, 或者如果收到领倒者移交来的事件,他会立刻转为处理中状态。
下图描述了状态的转化:

实现
下面几个步骤将实现L/F模式.
1.    选择一个IO句柄和句柄集机制。 句柄集是 IO句柄的集合,用于等待句柄上发生的事件。 开发者通常选择由操作系统提供的IO句柄机制,而不是自己白手实现。下面几个子步骤可用于选择IO句柄和句柄集的机制。
1.1     确定IO句柄的类型: 有两种常用的类型:
并发句柄   这种类型的句柄允许多线程并发的访问它,而不会由于资源竞争导致崩溃,数据丢失或数据混乱。 例如,Socket API 对面向记录的(record-oriented)协议(如UDP), 允许多线程对同一个句柄同时读写。

迭代(interative重复)句柄:  这种类型句柄要求多线程循序地访问它,因为并发访问会导致竞争条件。 例如Socket API 对面向字节流的协议(如TCP),不保证read和write操作是原子的。所以如果IO操作没有适当的进行串行化,将会破坏或者丢失数据。

1.2    确定IO句柄集合的类型: 也有两种:
并发句柄集合:该集合可以被并发的访问,例如,被线程池所调用。 当它可能在一个句柄上发起一个操作,且这个操作是非阻塞的,则将并发句柄集合返回给它的调用者。 例如,Win32的 waitForMultipleObjects 函数支持并发句柄集合,允许多个线程同时在一个句柄集合上等待事件。

迭代(interative重复)句柄集合: 这种类型集合当可能在一个或者多个句柄上发起非阻塞操作时,会返回一个迭代集合。尽管他能在一次调用中返回多个句柄,但不能被多个线程同时调用。 Select 和poll 只支持迭代集合,所以select /poll 都不允许多线程同时访问一个句柄集合。
1.3    确定对IO句柄和句柄集合的选择产生的影响
通常来说,LF模式被用于防止多线程错误地破坏或者丢失数据, 例如并发的读取一个TCP连接,或者同时在同一个句柄集上执行select(). 然而,有些应用不必关心这些情况。实际上,如果IO句柄和句柄集合机制都是并发的,下面的几个步骤都可以省略。
举例来说,特定的网络编程API, 如UPD Sockets,支持并发IO操作,因此消息总可以无风险的被线程读取,不必担心并行读取或交错的数据损坏。 同样地,特定的句柄集和机制,例如win32的waitForMultipleObjects 函数每次调用返回唯一的句柄,并允许多线程并行访问它。在这些情景下, 可以简单的使用操作系统的调度器来安全地复用线程、句柄集和和句柄。 这种实现可以省略掉 步骤2.(activity 2). 如果在步骤3中确定线程是非绑定的,那么其余步骤也都不必实现了。

1.4    利用高级模式将底层句柄集机制封装(可选) ,实现LF模式的一种思路是使用操作系统原生的事件多路分离机制,例如select 和waitForMultipleObjects. 相反的,开发者可以使用高级设计模式,如Reactor, Proactor, 和Warapperfacade。这些模式简化了LF模式的实现,并减少了为克服直接使用操作系统底层机制带来的额外复杂性锁做的付出。此外,应用高级设 计模式更易于将并发模型和系统的IO分离机制解耦,从而减少代码的重复和维护工作。
举个例子,在我们的OLTP服务器例子中, IO事件必须要被分离给实际的事件处理者,这要根据是哪个IO句柄接收的这个事件来分派。 Reactor模式支持这种行为,从而简化LF模式的实现。 在LF模式中,一个响应者(reactor) 在分离过程中只分派一个实际事件处理者,而不管有多少句柄将有事件发生。下面的c++代码描述了Reactor模式的实现:
Typedef unsigned int Event_Types;
Enum{
ACCEPT_EVENT = 01,//accept 事件是read事件的一个别名
READ_EVENT = 01,
WRITE_EVENT= 02, TIMEOUT_EVENT= 04,
SIGNAL_EVENT= 010, CLOSE_EVENT=020
};

Class Reactor{
Public :
//对特定的事件类型注册一个事件响应者
Int register_handler (Event_handler *eh, Event_Type et);
Int remove_handler(Event_handler *eh, Event_Type et);
//进入取事件循环的入口函数
Int handle_events(Time_value *timeout = 0);
};

开发者提供Event_Handler接口的具体实现:
Class Event_handler{
Pulbic : 
/**
Reactor 处理特定类型的事件时调用的回调处理方法
*/
Virtual int handle_event(HANDLE, Event_Type et) = 0;
//获取IO句柄的钩子函数
Virtual HANDLE get_handle(void) const = 0;
};

(C++代码看着真tmd头晕~_~````那个啥,是叫虚函数么? Holly shit)

2 实现一个临时激活/失效句柄的协议
当事件到达时,领倒者线程使该句柄暂时失效,使其不在句柄集和的考虑之列,然后推举一个新的领倒者,并继续处理这个事件。 暂时的使句柄在集合中失效可以防止竞争出现,否则当新的领倒产生,且事件被经处理时,竞争条件就可能出现:如果新的领倒在此期间等待事件(则刚刚的事件也 会通知到新的领倒者),它很可能错误地再次分派这个事件。等到事件被处理完毕,句柄被重新激活,以允许领倒者等待上面发生的事件。
在我们的OLTP例子中,这个协议由reactor的实现提供,如下:
Class Reactor{
Int suspend_handler(Event_handler *, Event_Type et);
int resume_handler(Event_handler *, Event_type et);
}

实现线程集合
为了推举跟随者作为领倒者,以及确定哪个线程是当前领倒者,LF的实现必须管理一个线程集合。两种策略可用:非绑定和绑定方式,如下描述:

//剩下的还未看完...

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多