5.4领导者/追随者(Leader/Follower)
1.问题
多线程是实现并发处理多事件的应用程序的一种常用技术。然而,很难实现高性能的多线程服务器应用程序。这些应用程序通常处理大量同时到达的多类型事件。为了有效地处理这种问题,有三个强制条件必须解决:
1)服务请求可以来自为每个已连接的客户机分配的多事件源(如多TCP/IP套接字句柄)。因此,一个关键设计强制条件是在线程和事件源间确定有效的多路分解关联。
2)为了将性能最大化,必须尽量减少引起与并发有关的开销。(如语境切换,同步化和缓存一致性管理)。特别地,为在多个线程间传递的请求动态分配内存的同步模型会在传统的多处理器操作系统中产生巨大的开销。
3)多路分解共享的事件源集合上事件的多个线程必须相互协作,以防止竞争条件。竞争条件可能出现在多个线程试图同时访问或修改某些类型的事件源的时刻。
2.解决方案
构造一个线程池,通过对到达事件源的事件多路分解,并向处理事件的应用服务同步地分配事件,依此轮流进行来共享事件源集合。
详细细节:设计一个线程池机制,允许其中的多个线程相互协作并在检测、多路分解、分配和处理事件时保护临界区。在这种机制中,每次允许一个线程(领导者)等待在事件源集合上出现一个事件。同时,其他线程(追随者)排队等待它们成为领导者的机会。当前的领导者线程从事件源集合检测到一个事件后,它首先将一个追随者线程提升为新的领导者,然后扮演处理线程的角色,对事件多路分解并分配给指定的事件处理程序,在处理线程中实现与应用有关的事件处理。在当前领导者线程在由所有线程共享的事件源集合上等待新的事件时,多个处理线程可以并发地处理事件。在处理完事件后,处理线程恢复到追随者角色,并等待再次成为领导者线程。
3.结构
由操作系统提供句柄,用来区分可以生成事件并将之排队的事件源(如网络连接或打开文件)。事件可以由外部事件源(如从客户机发送一个服务的CONNECT事件或READ事件),或者内部事件源(如超时)引发。句柄集是句柄的集合,可以用来等待一个或多个事件在该句柄集中的句柄上发表。当可以激活句柄集中句柄上的一个操作而不发生操作阻塞时,句柄集返回到它的调用者。
事件处理程序明确了由一个或多个钩子方法组成的接口。这些方法表示可以对发生在句柄上的与应用有关的事件进行处理的操作集。
具体事件处理程序是事件处理程序的特化,并实现应用程序提供的特定服务。特别地,具体事件处理程序实现负责处理从句柄接收的事件的钩子方法。
领导者/追随者模式的核心是线程池。一个或多个线程扮演追随者角色并在线程池同步器上排队等待扮演领导者角色。其中一个线程被选择成为领导者,等待事件在句柄集中的句柄上发生。当有一个事件发生时,当前领导者线程将一个追随者线程提升为新的领导者。原来的领导者接着扮演处理线程的角色,对从句柄集到相应事件处理程序的事件进行多路分解,并分配处理程序的钩子方法进行事件处理。在处理线程完成了事件的处理后,它再次返回扮演追随者线程的角色,在线程池同步器上等待再次成为领导者线程。
4.实现
1)选择句柄和句柄集机制。句柄集是句柄的集合,领导者线程可以用它来等待在事件源集上发生事件。开发者通常选择底层操作系统提供的句柄和句柄集机制,而不是随便地实现它们。
1.1)确定句柄类型。
·并发句柄。这种类型的句柄允许多个线程并发访问事件源的句柄而不会引发可能破坏,丢失或扰乱数据的竞争条件。
·迭代句柄。这种类型的句柄需要多线程迭代访问事件源上的句柄,因为并发访问将导致竞争条件。
1.2)确定句柄集的类型。
·并发句柄集。这种类型的句柄集上可以有并发的动作,例如,线程池对它的并发访问。
·迭代句柄集。这种类型的句柄集在它可以启动句柄集中一个或多个句柄上的一个操作而不阻塞操作时返回到它的调用者。虽然迭代句柄集可以在单个调用中返回多个句柄,但是它一次只能由一个线程调用。
1.3)确定选择某个句柄和句柄集机制的效果。
1.4)实现事件处理程序的多路分解机制。
·对低层操作系统事件多路分解机制编程。在这种策略中,直接使用操作系统提供的句柄集多路分解机制。
·对高层事件多路分解模式编程。在这种策略中,开发者使用诸如反应器、主动器和包装器材外观等高层模式。
2)在句柄集中实现临时激活(停用)句柄的协议。当事件到达时,领导者线程执行以下三步:
·在句柄集中暂时停用该句柄。
·将一个追随者线程提升为新的领导者。
·继续处理事件。
在句柄集中将该句柄停用避免了从选择新领导者到处理事件的时间内出现竞争条件。如果新的领导者在这一段时间内等待句柄集中的同一句柄,那么它可能再次将该事件多路分解,这样做是错误的,因为这时已经在进行分配了。在事件被处理后,句柄在句柄集中被再次激活,这样领导者线程等待事件在该句柄或句柄集中其他激活的句柄上出现。
3)实现线程池。为了将一个追随者线程提升为领导者角色,以及确定哪个线程是当前的领导者,领导者/追随者模式的工具必须管理一个线程池。一种直接的实现方法是简单地将所有追随者线程放入集合,等待单独的同步器,如信号灯和条件变量。在这种设计中,不管是哪个线程来处理事件,只要共享句柄集的池中的所有线程都是串行化的。
4)实现允许线程初次加入(以及以后再次加入)线程池的协议。该协议用于以下两种情况:
·在首创了能获得和处理事件的线程池后。
·以及在处理线程已完成并且可以处理另一个事件时。
如果没有领导者线程可用,则处理线程可以立即变为领导者。如果领导者线程已经获得,一个处理线程可以在线程池同步器上等待变成追随者。
5)实现追随者提升协议。
5.1)实现句柄集同步协议。如果句柄集是迭代的,并且我们盲目地提升了一个新的领导者线程,那么可能新的领导者线程会试图处理与前一个领导者线程检测到的并正在进行处理的同一事件。为了避免这种竞争条件,必须在将一个追随者句柄提升为领导者句柄并将事件分配给具体事件处理程序之前,将该句柄从句柄集的候选句柄中删除。分配并处理事件后,必须从句柄集中再次激活该句柄。
5.2)确定提升协议排序。
·LIFO顺序。在许多应用程序中,下一次提升哪个追随者线程不重要,因为所有线程都是对等的。在这种情况下,领导者线程可以按照后进先出(LIFO)的顺序提升追随者线程。LIFO协议通过确保等待时间最短的线程首先被提升将CPU高速缓存的相似性最大化。
·优先级顺序。在某些应用程序中,特别是实时应用程序中,线程可能运行在不同的优先级上。在这种情况下,需要根据追随者线程的优先级对它们进行提升。这种协议可以使用某些类型的优先级队列实现,如堆。虽然该协议比LIFO协议更复杂,但是为了使优先级反序的情况最少,就需要按照追随者线程的优先级对它们进行提升。
·由实现定义的顺序。在使用操作系统同步器(如信号灯或条件变量)来实现句柄集时常用这种顺序,通常按照由实现定义的顺序分配等待线程。这种协议的优点是它能高效地映射到本地操作系统同步器上。
6)实现事件处理程序。
5.结论
优点:
1)性能增加。
·增强了CPU高速缓存相似性并消除了动态内存分配和线程间共享的数据缓冲区要求。
·通过在线程间不交换数据的方法来使加锁开销达到最小,因此降低了线程同步化。
·可能将优先级逆序的数量减少到最小,因为服务器中没有进行其他排队。
·不需要语境切换以处理每个事件,减少了事件分配延时。
2)编程简单性。领导者/追随者模式简化了并发模型的编程,其中多线程可以使用共享句柄集接收请求,处理响应并多路分解连接。
不足:
1)实现复杂性。
2)缺乏灵活性。
3)网络I/O瓶颈。