面向模式的软件架构:并发

来源:互联网 发布:互穿网络防腐漆 编辑:程序博客网 时间:2024/04/30 07:34

 

选择什么样的并发架构会对多线程软件的设计和性能产生极大的影响,而对分布式软件的影响尤为明显。然而,还没有一种并发架构是适合所有的负载情况和平台的。本章所介绍的四种并发模式可以应对多种并发问题——从将异步并发处理与同步并发处理组合起来到将对共享组件的访问进行同步,同时保证性能和吞吐量的最大化。

  分布式系统软件通常能够从并发中获益,尤其是处理从多个客户端同时发出请求的服务器和服务器端软件。同时,人们设计出越来越多的多核CPU和多CPU计算机来运行多控制线程,以弥补相对于摩尔定律的差距[Sut05a]。因此,进程和线程管理机制成了分布式系统软件开发人员必须精通的知识。

  进程是一组资源的集合,比如虚拟内存、I/O句柄、控制线程,它为执行程序指令提供上下文。在硬件保护地址空间(hardware-protected address space)中,每个进程便是一个保护和资源分配单元。相反,线程是一个独立的指令序列,它以进程作为运行的上下文。线程不仅包含指令的指针,同时它还包括诸如函数激活记录(function activation records,即调用栈)、寄存器组以及线程专属(thread-specific)的数据等资源。一个线程是一个执行单元,它属于某个进程,并且与进程中其他的线程共享地址空间。

  促使分布式系统软件使用多进程和多线程的原因是多方面的,包括:

   通过使用现代硬件和软件平台的进程并发能力透明地提高性能。

   允许程序员进行交叠计算和在服务处理过程中交互,从而显式地提高性能。

   对于交互式——比如包含用户界面的——软件可以缩短感知响应时间,因为不同的线程执行不同的服务处理任务,用户可以在某些任务阻塞的时候做其他的工作

   允许多个服务处理任务独立运行,采用同步的编程抽象——比如双向方法调用,以及阻塞在I/O和锁上的操作——从而简化应用设计。

  然而,编写高效、可预料、可缩放而且鲁棒的并发软件是相当困难的[Lea99]。高效的并发编程绝不仅仅是把独立的组件、对象或者服务用各自的控制线程启动起来,然后就可以撒手不管了。究其原因,包括以下几点:

   软件的多样性。 既然不同类型的分布式系统软件的结构和行为特点各不相同,所以也就不存在放之四海皆适用的并发模型。例如,有些软件混合使用异步和同步服务处理,有些软件则由事件驱动,还有的软件必须处理不同优先级的服务请求。因此,每个类型的软件所要求的并发模型都可能有所不同,以便为用户提供有质量保证的服务,同时为开发人员提供合适的编程模型。

   多线程成本。 并发软件的设计者必须清楚多线程会引入上下文切换、同步和在CPU缓存间移动数据的开销。轻率地使用线程机制很可能会减少从并发中获得的好处,甚至得不偿失。因此,设计并发软件时应该尽量将应用多线程的开销降到最低。

   可移植性。已有的软件开发方法、工具和操作系统平台的局限也会给并发编程带来额外的复杂性。例如,现代硬件和软件平台的多样性使得开发能够运行在多种操作系统上的并发软件和工具变得异常复杂。

  有效地解决这些挑战和复杂性要求开发人员了解并能够正确地运用并发模式。在设计软件的基线架构、子系统和组件的整个过程中,都应该自觉地、认真地理解和应用这些模式。

  我们的分布式计算模式语言包含了4个模式,实践表明这些模式可以帮助我们创建多种并发架构和解决各种设计问题。它们是:

   HALF-SYNC/HALF-ASYNC模式(359)[POSA2]解耦合并发系统中的异步和同步服务处理以简化编程,而不会过度地影响性能。该模式引入两个相互通信的层,一个用于异步服务处理,另一个用于同步服务处理。

   LEADER/FOLLOWERS模式(362)[POSA2]提供了一个高效的并发模型,在该模型中多个线程轮流使用一套事件源,来检测、分离、分派和处理事件源中出现的服务请求。

   ACTIVE OBJECT模式(365)[POSA2]通过将服务请求和服务执行解耦合来提高并发性,它将对象化的服务请求放到自己的控制线程中,并简化了对它们的访问。

   MONITOR OBJECT模式(368)[POSA2]通过同步并发方法的执行来保证同一时刻一个对象中只有一个方法在运行。它允许一个对象的多个方法以协作的方式确定执行时刻表。

  本章的范围仅限于开发分布式系统并发通信的中间件和应用程序组件。我们的目的并非要覆盖与并发相关的所有方面。我们把主要的关注点放在几个关键的模式上面。这些模式定义了如何构造(structure)和分割(partition)并发软件——从而形成多个协作的线程,以及如何组织对由多个线程共享的组件的访问。如今在理论上和实践上已经存在很多成功的并发模型,但是这里我们有意没有作以笼统地、指导性地展示,因为这并非本章的目的所在。

  有关线程同步技术的模式也没有包括在本章,而是在第16章——同步中。 虽然THREAD-SPECIFIC STORAGE模式在[POSA2]中归类为并发模式,多年的使用经验告诉我们它更多的是关于如何避免加锁成本——而不是并发,因此我们把它也归到了同步这一章。

  MONITOR OBJECT归于并发模式还是同步模式也有些争议。我们把它放到了这一章,因为其主要的目的是在面向对象编程中应用并发:它也使用了互斥、条件变量等同步机制,但是相对来说并非其主要特点。值得指出的是MONITOR OBJECT是ACTIVE OBJECT的补充。

  本章中的模式分为两组:并发基础设施和访问同步。

  HALF-SYNC/HALF-ASYNC和LEADER/FOLLOWERS定义了高层并发架构。HALF-SYNC/HALF-ASYNC解耦合并发系统中的异步和同步处理以简化编程,而不会过度影响操作系统和网络层次的性能。由于其编程模型简单,HALF-SYNC/HALF-ASYNC被用于多种并发应用,包括操作系统、中间件、工业流程控制和通信应用[POSA2]。LEADER/FOLLOWERS为事件驱动系统提供了并发模型。它尤其适用于处理短暂的(short-duration)、原子的并且反复的动作中的事件,比如接收和分发网络事件或者向数据库中存储大量数据记录。

  展示了HALF-SYNC/HALF-ASYNC和LEADER/FOLLOWERS是如何与我们的模式语言集成在一起并应用于分布式计算的。

 

  ACTIVE OBJECT和MONITOR OBJECT模式可以对对象和组件上并发调用的函数进行同步和调度。其主要区别是ACTIVE OBJECT的方法是在与其客户端不同的线程中执行,而MONITOR OBJECT的方法则在其客户端线程中执行。所以ACTIVE OBJECT可以执行更为复杂——尽管代价很大——的调度,来确定其方法的执行顺序。ACTIVE OBJECT主要用于在大的组件和子系统中支持并发,而MONITOR OBJECT则主要用于实现较小的并发对象。

  展示了ACTIVE OBJECT和MONITOR OBJECT是如何与我们的模式语言集成在一起并应用于分布式计算的。

  15.1 Half-Sync/Half-Async **

  在开发并发软件,尤其是并发ENCAPSULATED IMPLEMENTATION(313)或者应用了REACTOR(259)或PROACTOR(262)作为事件处理基础设施的网络服务器时…

  …我们需要在确保对并发机制的应用能够简化编程的同时,还要保证性能上的高效和可伸缩性。

  ◆◆◆

  并发软件中的服务处理通常是异步和同步同时存在。异步用于高效地处理底层系统服务,同步则用于简化应用服务处理。要想从两种编程模型中均获益,高效地协调异步和同步服务处理是非常重要的。

  异步和同步服务处理通常是相关联的。例如,Web服务器的I/O层往往使用异步读操作来取得HTTP Get请求[HP97]。而在CGI层对GET请求的处理则同步地运行于独立的控制线程。在I/O层异步到达的请求必须与CGI层对请求的同步处理集成在一起。换一个角度,我们再来看看Web客户端,AJAX可以使用异步JavaScript和XML(asynchronous JavaScript and XML)来提高Web客户端的可感知响应速度[Gar05]。通常,异步和同步服务应该相互协作、取长补短。

  因此:

  将并发软件的服务分解为异步和同步两层,然后增加一个队列层来处理二者直接的交互。

  使用单独的线程或者进程以同步的方式处理高层服务,比如领域功能、数据查询或文件传输,而由网络硬件中断所驱动的短暂(short-lived)协议处理程序等底层系统服务则应该采用异步方式。如果同步层的服务必须和异步层的服务通信,则应当采用排队层交换消息的方式进行。

  ◆◆◆

  HALF-SYNC/HALF-ASYNC模式对这三层进行了严格的划分,这使得并发软件更容易理解、调试和改进。而且,异步和同步服务各自的缺陷也不会出现相互传染:异步服务的性能不会因为同步服务被阻塞而降低,同步服务的编程简单性也不会受到异步复杂性(比如显式的状态管理)的影响。最后,使用排队层可以避免对异步和同步服务层之间的依赖进行硬编码,同时也可以简化消息处理的优先级排序。异步层和同步层的严格解耦合要求这两层之间的通信必须或者使用COPIED VALUES——这样就会带来性能上的损失和资源管理上的消耗,因为有更多的数据需要传送;或者使用IMMUTABLE VALUES表示——这种做法相对来说属于轻量级的方式,但是其构造上可能更为复杂。

  总的来说,HALF-SYNC/HALF-ASYNC通过使用LAYERS(185)来保证三个不同的执行模型(model)和通信模型的独立性和封装性。

  同步层服务,比如数据库查询、文件传输或者领域功能通常运行在自己的线程中,以便多个服务可以同时执行。如果一个服务实现为ACTIVE OBJECT(365)它便可以同时处理多个服务请求,从而提高应用的性能和吞吐量。

  异步层的服务可以通过异步中断或者支持异步I/O的操作系统API实现,后者包括Windows重叠I/O(Windows overlapped I/O)和I/O完成端口(I/O completion ports)[Sol98],或者POSIX异步I/O aio_*族系统调用[POSIX95]。WRAPPER FACADES (459)用于将与平台相关的异步I/O功能封装成统一的接口,藉此简化异步层的正确使用和跨平台移植。如果我们将HALF-SYNC/HALF-ASYNC设计成与PROACTOR或者REACTOR事件处理基础设施联合使用,这个事件处理基础设施便是所谓的异步层。虽然REACTOR并非是真正的异步,但如果它的服务实现的是短暂(short-duration)操作,而不是长时间的阻塞,你就会发现它其实具有异步的关键属性。

  排队层通常是由同步层和异步层所有服务共享的消息队列组成。复杂的排队层可以提供多个消息队列,比如为每个消息优先级或者通信端提供一个消息队列。消息队列可以实现为MONITOR OBJECTS(368),这样就可以使得异步和同步服务能够对消息队列进行透明的串行访问。TEMPLATE METHODS(453)和STRATEGIES(455)用于支持对消息队列不同方面的设置,例如配置其排序、串行化、通知和流程控制的行为。STRATEGIES相对更为灵活,它可以为消息队列提供更松的耦合度,并支持运行时配置和重配置。TEMPLATE METHODS则更适合于仅需要编译期灵活性的情况。由排队层进行路由的信息封装在MESSAGES(420)里面。

  15.2 Leader/Followers **

  在开发并发软件,尤其是并发ENCAPSULATED IMPLEMENTATION(313)或者应用了REACTOR(259)作为事件处理基础设施的网络服务器时…

  …我们常常需要并行而高效地响应和处理从多个事件源发起的不同事件。

  ◆◆◆

  大多数事件驱动的软件使用多线程来并行地处理多个事件。然而,要想以高效的、可预见的和简单的方式为线程分配工作却出奇的困难。

  在事件驱动软件中,尤其是服务器软件中,经常需要在线程和事件源之间定义高效的事件分离机制。同时,在使用多线程分离一系列共享事件源上的事件的时候,还需要防止出现竞争状态。例如,一个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)配置中的服务处理器时…

  …我们通常需要确保组件的操作可以在它们自己的控制线程中并行的运行。

  ◆◆◆

  并发通过允许组件同时处理多个客户端请求而不必阻塞来提高软件服务质量。然而,如何在软件中表现并发单元,以及如何在运行中与之交互,则是由开发人员决定的。

  尤其,对于客户端来说,它应该能够随时向组件发出请求,而不必等着其他请求执行完毕。我们需要能够根据请求的优先级或者截止时间等特征调度客户端请求的执行。为了保证服务请求的独立性,它们的串行化和调度对组件和客户端都应该是透明的,以便在对软件实现进行重用的时候不必考虑它们是否要求不同的同步策略。

  因此:

  将并发单元界定为组件上的服务请求,并且让它和请求的客户端在不同的线程上运行。使客户端和组件可以异步地交互来产生和使用服务结果。

  客户端可以通过调用组件暴露给客户端线程接口上的方法初始化组件上的服务请求。设计组件接口的时候不必受同步的约束,在发出请求之后立刻把控制返回给客户端。将请求对象化,并传递给运行在一个或多个单独线程上的组件实现,让组件实现独立地调度服务请求的执行,而不依赖于何时对请求对象进行的初始化。此外,在服务结束后还要为组件提供将结果返回给客户的机制。

  ◆◆◆

  ACTIVE OBJECT设计通过允许客户端线程和服务请求同时运行来增强应用程序中的并发性。在服务请求执行的过程中客户端并不会被阻塞。而且,通过使用调度器减少了同步的复杂性,调度器可以根据一定的同步约束机制来达到对组件实现的串行化访问。从实际组件实现中将需求调度分离出来,这样我们的组件就可以在不要求同步的情形中重用。同时有些组件可能并不是为并发访问设计的,这种分离机制使它们可以在并发的应用中使用。最后,服务请求的执行顺序可以与服务请求的调用顺序不同,这样在采用优先级排序、截止时间和其他的同步约束时就更加方便了——不过这样做会使得调试更为复杂。ACTIVE OBJECT也会引入比较重量级的请求处理和执行基础设施,这对那些只实现短暂方法的组件会导致性能上的损失。

  使用EXPLICIT INTERFACE(281)将组件的接口暴露给客户端线程。从客户端的角度来看就跟组件位于客户端线程内是一样的。设计接口的时候要确保方法签名中不包含同步参数。这样即使并发组件是由多个客户端线程共享的,从客户端看来就像自己拥有独占的访问一样。

  在运行时,组件接口将所有的方法调用对象化为服务请求——这通常使用COMMAND(435)来实现——并使用它来完成相应的方法调用的同步约束机制。请求的对象化将服务请求和服务执行在时间和空间上进行了解耦合,这样当客户端调用组件上的服务时就不需要阻塞自己或其他的客户端。将创建的服务请求存储到一个共享的活动列表中,该列表维护了并发组件上所有挂起的服务请求。这个活动列表用MONITOR OBJECT(368)实现,可以确保并发访问是线程安全的。

  组件的实现可以有一个或多个宿主线程。在每个线程中,有一个实现组件的功能servant。调度器将服务请求对象从共享的活动列表中分离出来,然后在servant上执行。这样的设计允许服务请求和执行并行地运行,也就是说,服务请求在客户端线程中调用,而服务执行则运行在不同的线程中。而且,调度器将组件功能从调度和同步机制中分离出来,使得双方都可以独立的开发和改进。

  将调度器设计为COMMAND PROCESSOR(343)可以实现组件的事件循环(event loop)。它负责检查活动列表,发现可执行的服务请求后将其从活动列表中移除,并在servant上执行这个服务请求。我们还可以使用TEMPLATE METHODS(453)和STRATEGIES(455)来支持多种调度策略。TEMPLATE METHODS适用于调度器配置可以在编译期确定的情况。相反,STRATEGIES则支持在运行时配置和重配置调度策略。

  客户端可以通过FUTURE(382)获得并发组件上的服务请求结果。在服务调用之后,并发组件的接口将future返回给客户端,在servant完成服务执行之后,由关联的服务请求填充future。如果客户端在获得服务结果之前访问future,客户端可以阻塞或者轮询等待,直到服务结果有效。如果我们不再使用future,可以通过AUTOMATED GARBAGE COLLECTION(517)安全地将其回收——如果编程语言支持的话,否则,如果必须手动编码回收可以使用COUNTING HANDLE(522)。

  15.4 Monitor Object **

  在开发SHARED REPOSITORY(202)架构,REQUESTOR(242)、CLIENT REQUEST HANDLER(246)、MESSAGE CHANNEL(224)、MESSAGE ROUTER(231)分布式基础设施,ENCAPSULATED IMPLEMENTATION(313),ACCEPTOR-CONNECTOR(265)结构(?arrangement,有没有更好的词语),HALF-SYNC/HALF-ASYNC(359)、LEADER/FOLLOWERS(362)或者ACTIVE OBJECT(368)并发模型的时候…

  …我们必须考虑对象要能够被多个线程共享。

  ◆◆◆

  并发软件经常包含一些对象,其方法会被多个客户端线程调用。为了保护共享对象中的内部状态,有必要将客户端对它们的调用进行同步和调度。然而,为了简化编程,我们不希望客户端在编程的时候需要区分访问的组件是共享的还是非共享的。

  每个会被多个客户端线程访问的对象应该确保其方法的序列化是透明的,不需要客户端做显式地干预。为了确保其客户端的服务质量,如果共享的对象中的某个方法在执行的过程中必须阻塞,它应该自动地放弃其控制线程,保证组件处于稳定状态,以便其他客户端线程可以安全地访问。

  因此:

  执行共享对象(的方法)应该是在其客户端线程中进行,并让它能够自己协调这个串行化而又交错的执行序列。必须通过同步的方法对共享对象进行访问,确保同一时刻只能执行一个方法。

  每个Monitor Object包含一个监听者锁,用于访问对象状态时串行化。在一个同步的方法中,首先获得监听者锁,确保没有其他串行化方法可以执行。获得这个锁之后,检查共享对象的当前状态是否允许运行串行化方法。如果允许,则执行它,否则在某个条件上挂起对同步方法的执行。如果该方法的执行挂起在某个监听者条件上,监听者条件应该挂起调用者的线程,直到收到通知重新唤醒调用者线程。在线程挂起时,监听者条件应该释放监听者锁,并且在继续该线程的时候重新获取监听者锁。

  挂起同步方法允许其他线程通过其他同步方法访问共享对象。任何同步方法在其执行结束时都可能会对监听者条件的有效性产生影响。这时应当通知相应的监听者条件,以便挂起的方法可以继续执行。在结束一个同步方法之前要释放监听者锁,以便其他线程调用的其他同步方法可以执行。

  ◆◆◆

  将共享对象的类型设计为MONITOR OBJECT——通过在协作的线程中共享对象,并将方法调用和状态同步结合起来——简化了并发控制。MONITOR OBJECT对于实现方法执行序列之间的协作也有帮助,确保共享对象对客户端是可用的,并在序列化条件约束下最大化共享对象的可用性,确保其状态的变化是完整的而且不会带来竞争状态。然而,这个模式的缺陷是它把领域功能和同步功能紧密的耦合在一起。编写或者使用Monitor Object经常会带来死锁的问题。例如,如果一个Monitor Object回调了另一个对象,而这个对象使用了其他的Monitor Object,就很容易出现死锁。

  Monitor Object可以使用THREAD-SAFE INTERFACE(384)来将同步行为和其他功能解耦合。这样两者就可以独立地变化了。GUARDED SUSPENSION(380)可以用于协调对象中运行的线程。方法的执行是由监听者条件和监听者锁来调度的,它们决定它们在什么情形下应当挂起,什么情况下应当继续执行。

原创粉丝点击