《Pattern-Oriented Software Architecture, Patterns for Concurrent and Networked Objects》Vol.2 笔记

GoF的23种经典模式使得设计模式开始成为程序员的通用语言。《Pattern-Oriented Software Architecture, Patterns for Concurrent and Networked Objects》 Volume 2主要总结了并行系统中四大类16种设计模式。这里是一个摘录整理。

Service Access and Configuration

Wrapper Facade

它将底层的系统级API包装成加面向对象的,统一的和可移植的接口。像Thread, Mutex, Condition Variable, Socket之流的系统层C接口直接调用的话不仅可移植性差且容易出错。通过高级语言诸如C++可以把一些语义上的规则(如离开scope时释放锁,mutex/conition variable不可拷贝等)通过语言特性封装起来。像Android当中Looper,Thread, Mutex等都可以看作是该模式的应用。

Component Configurator

实现组件的动态加载和配置。主要包含四个部分:Component提供了统一接口,具体的Components提供了特定类型的实现,Component repository管理所有Concrete components,Component configurator使用Component repository来协调具体Components的配置。如Android中的ServiceManager采用了这种模式来管理系统中的Service。

Interceptor

允许应用通过注册符合预定接口的服务实现扩展Framework的目的。简单地说就是Framework提供的hook函数接口。首先,系统给出interceptor回调接口,而应用负责实现具体的interceptor。类似于Observer模式,具体Interceptor可以向Dispatcher注册,之后Dispatcher收到事件的时候,会将之分发到所有注册的interceptor。

Extension Interface

允许同一个Component暴露多个接口,从而防止Interface爆炸式增长。主要目标是改动Component的功能且不break已有的Client代码。解决方案是让Client通过单独的接口访问Component,而不是用统一接口。对于同一个对象,每个Client只看到它所需要的接口。比如Android 4.4中的BufferQueue类,既实现了生产者接口又实现了消费者,不同Client各取所需,改动互不影响。

Event Handling Patterns

Reactor(Dispatcher, Notifier)

对于事件驱动的应用,Reactor允许它可以对从多个Client发来的请求进行分路和分发。它可以从一个或多个事件源以同步方式等待事件的到来,并将分路和分发机制和应用的事件处理解耦。缺点是其伸缩性差,因为所有操作都是单线程串行执行的。如Android中的Looper,还有libev库,其中的消息循环都是Reactor模式的实现。

Proactor

和Reactor的纯被动等待方式不同,Proactor模式中应用组件会通过开始一个或多个异步操作请求来主动发起控制和数据流。而对服务请求的分路和分发是被异步操作结束事件所触发的。它需要OS对异步I/O的支持,如Windows中的GetQueuedCompletionStatus()。

当Client向Service发起一个异步操作请求,就创建一个ACT。它是completion handler的闭包,其包含了指定completion handler所需要的所有信息。然后会被连同请求一起传给Service。Service不会修改它,在申请完成时,会传回给Client。Android中的MessageHandler就是同步ACT变体的例子。

Acceptor-Connector

Acceptor-Connector模式将连接及初始化与之后的服务处理解耦。大体上,两个工厂Acceptor和Connector用于创建Service handler的连接。当连接建立后,Acceptor和Connector工厂初始化相应的service handler。之后Service handler就可以通过其中的transport handle来交换数据和执行操作了,并不再需要与Acceptor或Connector工厂发生交互。举例来说,在Android中,Client要与SurfaceFlinger连接的话,要通过ComposerService向SurfaceFlinger请求连接,SurfaceFlinger返回SurfaceComposerClient。然后SurfaceComposerClient就可以和SurfaceFlinger中的Client直接通信了。

Synchronization Patterns

Scoped Locking(Synchronized Block, Resource-Acquisition-is-initialization, Guard, Execute Around Object)

一般的锁实现的问题之一是它不是exception-safe的。一旦丢出异常或调用函数丢出异常,它会不释放锁就返回。结果就是可能会产生死锁。该模式基于C++中的析构函数,保证当控制流进入scope时获得锁,而当离开scope时释放锁。Android里的Mutex::Autolock和C++里的lock_guard皆属于此类。

Strategized Locking

通过Strategy模式将同步机制参数化。同步策略可以是mutex, rw/lock或semaphore等。可用多态或是模板参数实现。前者适于运行时才知道策略的,后者适用于编译时的。

Thread-Safe Interface

Thread-Safe Interface模式的主要目的是防止组件间调用的self-deadlock。具体地,将对象方法分为接口层和实现层。接口层函数一般是公有的,它们负责获得释放锁和同步检查,然后调用相应的实现层函数。实现层函数假定调用者已经获得相应的锁,而且不会往上调用到接口层的方法(否则会死锁)。很多系统中函数后面跟Locked或者_l后缀代表是实现层函数。

Double-Checked Locking(Lock Hint)

该模式用于在只需执行一次的临界区代码中减少竞争和同步开销。比如一段只需被执行一次的临界区需要同步,如果简单加锁意味着以后每一次都需要检查锁,这是额外的开销。典型的比如Singleton中的instance初始化。基本思想是在进入临界区前做两次检查,一次在加锁前,一次在加锁后。这样,当第二次控制流来到时,在加锁前的检查就跳转了,减去了锁的开销。POSIX中mutex基本已经用futex实现,从而降低无竞争时锁的开销。但在性能关键的场合,比如kernel中,它还是被广泛应用的。

Concurrency Patterns

Active Object(Concurrent Object)

Active Object模式将函数执行和函数调用解耦,以简化不同线程中对于共享状态的同步。函数的调用在调用者所在线程,而函数的执行在单独线程,即该线程程为Active object的Scheduler线程。因此函数调用和执行可以并行进行。Proxy将函数调用转为调用请求,放在Scheduler的Activation list中。Servant提供这个Active object的实现。比如在单UI线程的系统中,UI控件是Active object,其它工作线程需要修改时都会请求主线程来执行。

Monitor Object( Thread-safe Passive Object)

Monitor Object保证无论有多少个线程在这个对象上执行,每个时刻在对象中只有一个方法执行。Active Object和Monitor object模式都可以用于调度对象上方法的并行执行。区别在于前者在单独线程,后者没有。前者的优点是可以实现一些调度策略。缺点是有上下文切换和数据移动的额外开销。例如Android里的很多需要并行访问的类都有一个Mutex对象,在公有接口调用时都会先对其加锁。

Half-Sync/Half-ASync

Half-Sync/Half-ASync主要将同步I/O操作和异步I/O操作解耦,并且通过加一个队列层来调节同步层和异步层之间的通信。具体来说,对于一些耗时操作,以同步方式放到单独线程中,从而简化并行模型。对于一些短时操作(Signal handler),以异步方式来调用提高性能。如果它们之间要通信,则通过队列层来传递消息。简单来说,下层拿到请求后放入队列,上层拿出处理。 例子比如Android中的AsyncTask。

Leader/Followers

多个线程轮流共享一组事件源,并进行检测,分路,分发和处理这些事件源上的服务请求。该模式的核心是一个线程池。具体地,每次只有一个线程(Leader)等待一组事件源。其它线程(Followers)排列等待成为Leader。当检测到到有事件来时,Leader线程成工作线程,它完成事件的分路和分发。最终调用Event handler。多个处理线程可以并行工作。当处理完后,工作线程又变回Follower线程,等待再一次变成Leader线程。Java中的ScheduledThreadPoolExecutor和DelayQueue均采用此模式。

Thread-Specific Storage

Thread-Specific Storage模式可以让多个线程访问“逻辑”上的全局数据,但其物理对象是本地的。这样就不会引起同步开销。一般会为Thread-Specific Storage的访问写Proxy来将TLS封装起来。这个模式的使用就不胜枚举了,比如错误代码,或者Android中的Looper,都是放在TLS中的。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。