Reactor Model

简介

Reactor模型是一种在事件模型下的并发编程模型。javascript

Reactor模型首先是一个概念模型;它能够描述在Node.js中全部的并发和异步编程行为,包括基础的异步API,EventEmitter对象,以及第三方库或实际项目中的已有的异步过程代码。java

其次,Reactor模型定义了一组简单但严格的设计规则,这些规则涵盖异步过程的行为、状态、和组合设计。这些规则能够用于设计和实现一个Node.js应用的所有行为逻辑。react

对于已有的Node.js API、库和项目代码,大多数状况下它们的行为和状态设计已经遵循了Reactor模型的要求,可是在组合实现上,可能一些代码不符合要求,但能够经过重构使之符合Reactor模型规范,一般没必要大面积重写。git

第三,Reactor模型没有提供任何代码,包括库函数或者基础类代码,也不要求统一的代码形式,只要应用Reactor模型的设计规则便可。算法

Reactor模型是与Process模型对等的并发模型,它适用于使用事件模型和支持异步io的编程环境例如,Node.js。和设计模式(Design Pattern)中的解决某类问题的行为型模式相比,Reactor是事件模型下通用的行为模型,因此咱们不把Reactor模型看做一种设计模式,设计模式能够在Reactor模型之上实现。spring

动机

Node.js已经问世8年,但对不少开发者而言它仍然是一个较新的语言环境;它独特的结合了事件模型和非阻塞io,非阻塞性在代码中经过代码单元的组合,把几乎全部代码都变成了异步形式。编程

对基于Process模型实现并发编程的问题,既不缺少抽象的理论模型,也不缺少解决各类具体问题的设计模式,且不限于开发语言,甚至存在语言针对高并发问题设计(例如Go);可是在事件模型和结合了异步过程的Node.js中,我在近两年的高强度开发中几乎没有读到过在这方面具备统一的模型抽象、系统、全面、同时象GoF设计模式那样直接指导编程实践的文章。设计模式

这是创建Reactor模型和写下这篇文章的目标。promise

方法论

咱们从三个方面阐述Reactor模型:网络

  1. 什么是Reactor?它是什么?有什么基础特性?
  2. 如何实现组合模式?经过组合咱们不只仅能够用Reactor描述一个组件的行为,还能够用它来描述整个应用。
  3. 如何使用Reactor模型应对并发编程中的各类问题?

这篇文章阐述1和2的内容,3是下一篇文章的内容。

Reactor

一个Reactor表示一个正在执行的过程,它是一个概念【见脚注1】;和Process模型中的Process概念同样,是对一个过程的抽象。

在实际的Node.js代码中,程序运行时每次调用一个异步函数,或者用new关键字建立了一个表示过程的对象,在应用中都建立了一个Reactor;在这个异步函数执行结束,或者过程对象抛出最后的事件(一般为finish或者close)时,这个Reactor的生命周期就结束了;无论是否称之为Reactor,开发者对它并不陌生。

咱们用略微严格的方式来看这个有生命周期的动态过程,具备哪些特性和设计规则。

特性(Properties)

一个Reactor具备五个基本特性:

  1. Reactor是反应式的(Reactive);
  2. Reactor具备输入输出(io);
  3. Reactor是异步的(asynchornous);
  4. Reactor是有态的(stateful);
  5. Reactor是动态的(dynamic);

Reactive

Reactor本质上是状态机(state machine);虽然应用Reactor模型不强制要求开发者使用设计模式中的State Pattern代码形式,但要求开发者为每一个Reactor在内心构建一个状态机模型。

Reactor的内部行为用State/Event来描述,它是Reactor的实现(Implementation)方法。在一个Reactor收到事件时——这个事件可能来自内部,也可能来自外部——它会根据状态机行为设计更新本身的状态,这个过程咱们称为React或Reaction。

Input & Output

Reactor具备输入输出;在形式上:

  • 对于一个异步函数,咱们能够说它的参数是输入,返回结果是输出(包括错误);
  • 对于一个EventEmitter对象,调用它的方法能够看做是输入,它emit的事件均可以看做输出;
  • 若是一个对象向外提供callback形式或者async形式的异步方法,那么调用该方法能够看做输入,该方法的返回能够看做是输出;

与内部的State/Event状态机实现不一样,input/output描述的是Reactor的外部界面(interface)。

一个Reactor的input,在内部看均可以理解为事件,虽然其中一些可能致使Reactor的状态迁移,而另外一些不会;在讨论Reactor的状态机行为时,咱们使用input event或external event表示Reactor收到的外部事件。

Reactor能够产生output,用于输出数据或通知外部本身发生了状态迁移;Reactor模型主要关心后面一种状况。

Reactor能够组合,其组合方式和OO中的组合逻辑同样,一样的咱们把一个Reactor包含的Reactor称为其成员(member)。成员抛出(output)的事件对于Reactor的内部状态机实现而言是事件,但它不是input,由于它来自内部。咱们使用internal event表示一个Reactor收到的来自成员的事件。

Asynchronous

在任何状况下,一个Reactor都不容许在input时同步产生output。这就是Reactor模型中对异步的定义,它被定义成了一个Reactor的内禀属性。

这个要求对于异步函数来讲是一种常识;但对于EventEmitter对象而言,一些实际代码并无作到这个承诺(甚至没有试图这样去作);在Reactor Model中,这个行为是强制要求的。

Stateful

Reactor是有态的。

在实现组合(包括有并发的组合)时,Reactor须要清楚的知道每一个成员的状态,至少一个成员是否在进行中仍是已结束是必须清楚的,因此Reactor的状态迁移定义至少是running -> stopped

和input/output同样,这里说的状态指的是Reactor的外部状态,而非其内部实现的完整状态机状态。由于Reactor是对过程的通用抽象,在绝大多数状况下,在外部看只需为其创建不多的状态,例如:正常运行、已经发生过错误、已经被强制终止,已经结束等等。

咱们用显式状态(explicit state)一词来表述Reactor的外部状态【见脚注2】。

Dynamic

Reactor是动态的包含两个意思:

  1. 它能够被动态建立和销毁的【见脚注3】。
  2. Reactor是运行时概念,而不是代码层面的编译时的概念;若是用面向对象编程来类比,它对应object,而不是class。

Summary

Reactor不是一个高深或复杂的概念,它甚至能够认为是从代码中总结出来的。

输入输出和动态性写在这里是为了看起来略微严谨,实际上几乎全部的模型的基础构件,Process,Object, Actor等等,都有输入输出和动态性。

Reactive特性是Reactor的内部实现要求,写在这里是为编程考虑;若是要严格(形式化)定义Reactor,它不是必须的要素,由于Reactor概念是黑盒抽象,如何实现是白盒特性。

因此Reactor真正特别的地方只有两点:

  1. 它封装了异步的概念,在Reactor模型中只有这样一个异步定义,毫无歧义,并且能够说是毫无实现负担;
  2. 它显式有态,可是不复杂;创建显式状态的惟一目的是为了实现组合,它与一个Reactor的目的或实现细节无关,应该把它理解为一种语法(Syntax)而不是一种语义(Semantics)。

简单的说,Reactor表示一个异步和显式有态的过程。

代码示例

在Node.js里Reactor一般有两种代码形式:

  1. 异步函数
  2. EventEmitter的继承类

下面咱们看看如何把它们理解成Reactor,其中一些代码例子展现了简单的封装。

异步函数

fs.readdir('some path', (err, data) => {
  ...
})

调用一个异步函数能够建立一个Reactor(实例)

把一个异步看做一个Reactor的构造函数,这看上去有些古怪。但咱们应该这样理解:虽然这个异步函数可能没有返回任何对象引用,可是调用以后系统实实在在的启动了一个过程,这个过程在将来会对系统行为产生影响,调用过和没调用过,系统总体的状态不同。若是咱们用状态来描述系统,它是系统状态的一部分。

这里有个哲学问题能够说一下,一般咱们说状态的本质是系统的输入(事件)历史。假如系统没有创建任何状态模型,理论上,若是系统记录了所有的输入历史,不考虑性能,系统老是能够完成一样行为的;从这个意义上说,状态的本质是输入历史。

可是在调用一个异步函数后,咱们说有一个将来(Future)是系统状态的一部分,这个将来是系统自身的历史行为的形成的,但它并不是是一个输入的历史。

换一个角度看,在JavaScript里调用一个异步函数等价于建立一个promise,处于pending状态,那么这个promise能够看做是这个Reactor的一个显式表达。它符合Reactor的四个定义。

异步函数的返回,能够看做它产生了output,这个output除了能够返回error/data(也能够不返回任何值),更重要的,它向外部emit了一个finish事件。

在状态约定上,这种Reactor的状态数量是最少的,只有两个状态:在执行,记为S,和结束,记为0;它的状态迁移图能够简单的写成:

S -> 0

其中->符号用于表示一种自发迁移,即这种迁移不是由于使用者经过input强制的。

Spawn a Child Process

let x = child.spawn('a command')
x.on('error', err => { /* do something */ })
x.on('message', message => { /* do something */ })
x.on('exit', (code, signal) => { /* do something */})

// somewhere
x.send('a message')

// elsewhere
x.kill()

一个ChildProcess对象能够看做一个Reactor;调用它的sendkill方法都视为input,它emit的error, message, exit等事件都应该看做output;

ChildProcess对象的状态定义取决于执行的程序(command)和使用者的约定。Node.js提供的ChildProcess是一个通用对象,它有不少状态设计的可能。

S -> 0

考虑最简单的状况:执行的命令会在结束时经过ipc返回正确结果,若是它遇到错误,异常退出。在任何状况下咱们不试图kill子进程。在这种状况下,它能够被封装成S->0的状态定义。

const spawnChild1 = (command, opts, callback) => {
  let x
  const mute = () => {
    x.removeAllListeners()
    x.on('error', () => {})
  }

  x = child.spawn(command, opts)
  x.on('error', err => {
    mute()
    x.kill()
    callback(err)
  })

  x.on('message', message => {
    mute()
    callback(null, message)
  })

  x.on('exit', () => {
    mute()
    callback(new Error('unexpected exit'))
  })
}

在这里咱们不关心子进程在什么时间最终结束,即不须要等到exit事件到来便可返回结果;设计逻辑是error, messageexit是互斥的(exclusive OR),不管谁先到来咱们都认为过程结束,first win。

这段代码虽然没有用变量显式表达状态,但应该理解为它在spawn后马上进入S状态,任何事件到来都向0状态迁移。

按照状态机的设计原则,进入一个状态时应该建立该状态所需资源,退出一个状态时应该清理,在这里应该把event handler看做一种每状态资源(state-specific resource);在不一样状态下,即便是相同的event名称也应提供不一样的函数对象做为event handler(或不提供)。

因此mute函数的意思是:清除在S状态下的event handlers,装上0状态下的event handlers。使用Reactor Model编程,这种代码形式是极推荐的,它有利于分开不一样状态下事件处理逻辑的代码路径,这和在Object Oriented语言里使用State Pattern分开代码路径是同样的工程收益。

S => 0

咱们使用a ~> b来表示一个Reactor能够被input强制其从a状态迁移至b状态,用a => b表示这个状态迁移既能够是input产生的强制迁移,也能够自发迁移。

习惯上咱们使用S,或者S0, S1, ...表示过程可能成功的正常状态,用E表示其被销毁或者发生错误可是还没有执行中止。

S => 0的状态定义的意思是Reactor能够自发完成,也可能被强制销毁。

原则上是不该该定义S ~> 0或者S => 0这样的状态迁移的,而应该定义成S ~> E -> 0或者S => E -> 0,由于大多数过程都不可能同步结束,他们只能同步迁移到一个错误状态,再自发迁移到结束。

但常见的状况是一个过程是只读操做,出错时不须要再考虑后续行为,或者在强制销毁以后,其后续执行不会对系统将来的状态产生任何影响,那么咱们能够认为它已经结束。

容许这样作的另外一个缘由是能够少定义一个状态,在书写并发时会相对简单。

在这种状况下spawnChild2能够写成一个class,也能够象下面这样仍然写成一个异步函数,但同步返回一个对象引用;若是使用者只须要destroy方法,返回一个函数也是能够的,但将来要给这个对象增长新方法(input)就麻烦了。

const spawnChild2 = (command, opts, callback) => {
  let x
  const mute = () => {
    x.removeAllListeners()
    x.on('error', () => {})
  }

  x = child.spawn(command, opts)
  x.on('error', err => {
    mute()
    x.kill()
    callback(err)
  })

  x.on('message', message => {
    mute()
    callback(null, message)
  })

  x.on('exit', () => {
    mute()
    callback(new Error('unexpected exit'))
  })

  return {
    destroy: function() {
      x.mute()
      x.kill()
    }
  }
}

这里的设计是若是destroy被使用者调用,callback函数不会返回了,这样的函数形式设计可能有争议,后面会讨论。

S -> 0 | S => E -> 0

这是一个更为复杂的状况,使用者可能指望等待到子程序真正结束,以肯定其再也不对系统的将来产生任何影响。这时就不能设计S => 0这样的简化。

S -> 0和前面同样,表示子程序能够直接结束;S => E表示子程序可能发生错误,或者被强制销毁,进入E状态;E -> 0表示其最终自发迁移到结束。

在这种状况下,须要使用EventEmitter的继承类形式了。

class SpawnChild3 extends EventEmitter {

  constructor(command, opts) {
    super()

    let x = child.spawn(command, opts)
    x.on('error', err => { // S state
      // s -> e, we are not interested in message event anymore.
      this.mute()
      this.on('exit', () => {
        mute()
        this.emit('finish')
      })

      // notify user
      this.emit('error', err)
    })

    x.on('message', message => { // S state
      // stay in s, but we don't care any further error
      this.message = message
      this.mute()
      this.on('exit', () => {
        this.mute()
        this.emit('finish')
      })
    })

    x.on('exit', () => { // S state
      this.mute()
      this.emit('finish', new Error('unexpected exit'))
    })

    this.x = x
  }

  // internal function
  mute() {
    this.x.removeAllListeners()
    this.x.on('error', () => {})
  }

  destroy() {
    this.mute()
    this.x.kill()
  }
}

stream

Node.js有很是易用的stream实现,各类stream均可以理解成一个Reactor,只是状态更多一点。

对Readable stream,一般前面的S -> 0 | S => E -> 0能够描述其状态。在Node 8.x版本以后,有destroy方法可用。

S1 -> S2 -> 0 | (S1 | S2) => E -> 0

对于Writable stream,S状态可能须要区分S0和S1,区别是S0是end方法还没有被调用的状态,S1是end方法已经被调用的状态。

区分这两种状态的缘由是:在使用者遇到错误时,它可能但愿还没有end的Writable Stream须要抛弃,但已经end的Writable Stream能够等待其结束,没必要destroy。

在这种状况下,严格的状态表述能够写成S1 -> S2 -> 0 | (S1 | S2) => E -> 0

Node.js里的对象设计和Reactor Model的设计要求高度类似,但不是彻底一致。通常而言stream不须要再封装使用。实际上熟悉Reactor Model以后前面的spawn child也没有封装的必要,代码中稍微写一下是使用了哪一个状态设计便可。

Summary

在实际工程中,实现一个Reactor应该尽量提供destroy方法和诚实汇报结束(finish)事件;不然通过组合后的Reactor将没法完成这两个功能。若是不从最细的粒度作好准备,在粗粒度上销毁一个复杂组件并等到其所有内部过程都完成清理和结束,就会成为没法完成的工做。

在这一节里咱们看到了四种最基本的显式状态定义:

S -> 0                                    # 能够表示简单的异步过程
S => 0                                    # 能够表示能够取消的只读操做或网络请求
S -> 0 | S => E -> 0                    # 能够表示绝大多数以结果为目标,会发生错误和可销毁的过程
S0 -> S1 -> 0 | (S0 | S1) => E -> 0        # 能够表示Writable Stream等须要操做才能够结束的过程

对于工程实践中的绝大多数状况,这四种显式状态定义就够用了。

组合(Composition)

在上一节咱们把在Node.js代码中调用异步函数或new关键子定义为建立Reactor;在实际代码中,除了Node.js API以外的异步函数或者EventEmitter类都是开发者本身定义的,在代码上,它们须要调用其余的异步函数或者建立类成员实例来实现;在这一节,咱们来理解父函数或者父类建立的Reactor,和它使用的子含说或者子类建立的Reactor对象之间的关系,这种关系也是动态的,咱们把它定义为Reactor的组合。

组合指的是两个Reactor之间的包含关系,借用OO编程里的术语,咱们把被包含的Reactor称为成员。有时候咱们也会用父Reactor和子Reactor来表述这种关系。

组合关系不是一种并发关系,这就像一个函数调用了另外一个函数,咱们不会说他们是并发的。

组合关系是一种动态关系,由于Reactor是一个过程,在运行时,它的子过程(即成员)是不断的被建立和结束的。

例子

在Node.js中书写一个异步函数或者EventEmitter,都是在实现组合,这里给两个例子。

异步函数

const lsdir = (dirPath, callback) => {
  fs.readdir(dirPath, (err, entries) => {
    if (err) return callback(err)
    if (entries.length === 0) return callback(null, [])
    let count = entries.length
    let stats = []
    entries.forEach(entry => {
      let entryPath = path.join(dirPath, entry)
      fs.lstat(entryPath, (err, stat) => {
        if (!err) stats.push(Object.assign(stat, { entry }))
        if (!--count) callback(null, stats)
      })
    })
  })
}

这段代码中的执行分为两个阶段,先是readdir过程,readdir结束后(可能)并发一组lstat过程,每一个readdir或者lstat过程都是一个Reactor,他们都和lsdir过程构成了组合关系。在这里全部的Reactor都采用了异步函数的形式,也都采用了S -> 0的状态定义。

Emitter

class Hash extends EventEmitter {
  
  constructor(rs, filePath) {
    super()
    this.ws = fs.createWriteStream(filePath)
    this.hash = crypto.createHash('sha256')
    this.destroyed = false
    this.finished = false

    this.destroy = () => {
      if (this.destroy || this.finished) return
      this.destroyed = true
      rs.removeListener('error', this.error)
      rs.removeListener('data', this.data)
      rs.removeListener('end', this.end)
      this.ws.removeAllListeners()
      this.ws.on('error', () => {})
      this.ws.destroy()
    }

    this.error = err => (this.destroy(), this.emit(err))    
    this.data = data => (this.ws.write(data), this.hash.update(data))
    this.end = () => (this.ws.end(), this.digest = this.hash.digest('hex'))

    rs.on('error', this.error) 
    rs.on('data', this.data)
    rs.on('end', this.end)
    ws.on('error', this.error)
    ws.on('finish', () => (this.finished = true, this.emit('finish')))
  }
}

这段代码中Hash接受两个参数,一个Readable Stream对象和一个文件路径,它把stream写入文件,同时计算了sha256,保存在this.digest中。

具体的代码逻辑不重要,这里Hash能够构造一个过程对象,它会在内部建立一个write stream过程和一个hash计算过程,对应的Reactor的组合关系和对象和成员的组合关系是一致的,这是使用class语法的好处。

这个例子中组合获得的Hash,采用了S -> 0 | S => E -> 0的定义。(实际上这段代码有bug,在filePath指向了一个目录而非文件时,但做为例子这里暂时忽视这个问题。)

Reactor Tree

上面的例子看起来更象在解释代码,没有额外逻辑;事实也是如此,咱们并不试图创造新写法,只是从Reactor组合的角度去看程序运行时过程之间的关系。和Reactor是一个运行时的动态对象同样,这个组合关系也是运行时动态的。

用Reactor组合去理解整个Node.js应用:

  1. 整个应用能够理解为“最大”的Reactor;
  2. Node.js的异步API能够理解为最小的Reactor,这些Reactor的内部实现是Node.js run-time提供的,在应用中看不到,应用中只能看到它的界面,即输入输出;
  3. 在二者之间,每个在执行的异步函数或建立的EventEmitter对象,均可以用Reactor组合来解释;

一个在运行的Node.js应用,在任什么时候刻,都存在这个由过程和过程组合构成的层级结构(Hierarchy),在Reactor模型中咱们把这个运行时的过程和过程的组合关系构成的层级结构称为Reactor Hierarchy,或者Reactor Tree

并发

在Reactor Tree上,咱们能够得到并发的第一个定义。

若是在任什么时候刻这个Tree都退化成一个单向链表,程序就回到了单线程、使用blocking i/o编程和运行的方式,在Process/Thread模型中,它被称为Sequential Process;若是存在时刻,至少有一个Reactor存在两个或两个以上Children,或者等价的说,整个tree存在两个或两个以上Leaf Node,这个时候咱们说它存在并发

这个并发定义是从现象观察获得的,它从Reactor(或过程)的组合关系来定义,具备数学意义上的严格性;可是它没有区分一个Reactor所表示的过程到底处于何种状态,是一个在执行的io,仍是一个计算任务;二者的区别在于前者几乎不占用CPU资源,然后者可能产生显著的计算时间,在调度任务时,后者可能会形成其余任务的starving(长时间拿不到CPU资源),甚至致使系统彻底不可用。

可是从概念模型角度说,咱们接受这个并发定义,它简单纯粹,没有歧义。

状态通信协议

即便不创建严格的模型和术语体系,在直觉上,用朴素的过程和过程组合来理解Reactor Tree的行为,咱们也能够预见到在程序的运行时,每一个过程在不断的抛出事件,它的父过程接受和处理事件,构成运行时的执行流程

咱们创建Reactor模型的目的,就是要显式表述和充分理解这个执行流程, 它首先是设计的一部分,开发者须要给出其精确和完备的定义;其次,它在组合的过程当中应该遵循一些简单规则,使这个流程尽量容易理解、容易设计、容易调试、减小设计和实现的错误;这个执行流程不能是模糊的,或者在运行时陷入混沌(Chaos),包括在软件工程中逻辑单元和系统规模在不断的增加,逻辑变得愈来愈复杂时。

这是咱们创建Reactor模型和定义Reactor组合关系的初衷;为了让Reactor之间的交互和整个Reactor Tree的执行流程更加简单、可靠、和有序,咱们须要设计一套在组合Reactor时,Reactor之间的交互和通信须要遵循的逻辑。咱们把这套规则,称为Reactor模型的状态通信协议

严格的说,咱们在前面定义的Reactor时,其输入输出、异步、和有态特性,都是这个状态通信协议的一部分;可是咱们这里不追求形式化意义上的严格,咱们把上述特性留给Reactor的界面定义,把其他的部分做为状态通信协议定义。

Reactor的组合模式具备良好的递归特性和黑盒特性,即在各个粒度上均可以实现再组合,也能够在各个粒度上把一个Reactor当成(有态)黑盒看待,因此咱们只须要定义在一层组合关系下的通信协议。

在Reactor组合中,父子过程之间的通信应遵循下述协议要求:

  1. 子Reactor若是由于内部事件触发显式状态迁移,必须emit事件通知父Reactor;

    1. 子Reactor必须先完成状态迁移(reaction),而后才能emit;
    2. emit必须是同步的;
    3. emit的事件必须代表子Reactor刚刚迁入的显式状态;
  2. 子Reactor若是由于外部事件触发显式状态迁移,禁止emit事件;

    1. emit事件违反Reactor的异步要求;
    2. 若是外部事件会触发显式状态迁移,必须是一次强制迁移;即父Reactor调用子Reactor的方法强制子Reactor迁移至某个状态,这是该方法承诺实现的,且迁移是同步的;

规则1是子Reactor发生自发(触发来自内部)的显式状态迁移时的行为要求和对对父Reactor作出的状态承诺;规则2是父Reactor强制子Reactor状态迁移时子Reactor的行为要求和状态承诺。这两种状况中父子Reactor之间的交互,称为Reactor组合中的状态通信

在第一种状况中的状态通信的代码形式是异步函数返回或者emit事件时调用(父Reactor提供的)callback函数;在第二种状况中是父Reactor调用子Reactor的(同步)方法。

在Reactor模型的组合模式下,父子Reactor的通信必须是同步的。

连锁反应

一个Reactor的内部事件能够产生它的内部状态更新,这是Reaction;若是这个状态更新致使其显式状态迁移,按照通信协议设计,它应该向父Reactor抛出事件,这是Communication,这个事件对父Reactor来讲是内部事件,父Reactor一样作出Reaction。

这个过程能够迭代下去,成为连锁反应(chained reaction)。

在Reactor Tree上,上述连锁反应是自下而上的;它也能够自上至下,例如在父Reactor接收到data事件时,它判断数据有错误,所以强制销毁子Reactor;

这个也可能影响到并发的Reactor;例如父Reactor具备两个并发成员,a和b,在a抛出error事件时,父Reactor决定销毁并发的b过程(例如前面Hash的例子中,rs的错误处理中销毁ws);

回到整个Reactor Tree上考虑这种连锁反应;Node.js事件循环的一个tick,老是从一个基础异步API的callback开始,即最小的和原子的Reactor开始向整个应用抛出事件。

这个事件可能向上传播产生连锁反应,在向上传播的过程当中可能产生向下传播,可是它不可能在向下传播的过程当中再次产生向上传播,这是Reactor的异步特性保证的。换句话说,Reactor的异步特性保证整个Reactor Tree的Reaction是能够结束的。

若是Reactor没有这种异步保证,Reactor Tree上就可能出现循环(cyclic)的reaction;在理论意义上说它是livelock,在实践意义上说它是stack-overflow。

Reactive

在Reactor的基础特性中咱们定义了一个Reactive,它指的是对一个Reactor的内部实现使用状态机方法,它响应(React)外部事件更新状态,做为它的行为定义。

在组合模式下咱们看到第二个Reactive的含义:在Reactor Tree上,执行流程是以连锁反应的方式构成的。这和咱们在Process模型下,top-down的方式控制执行流程的方式彻底相反,它是bottom-up的。

在这个意义上说,不只仅是一个Reactor单元是Reactive的,整个应用都是用Reactive的方式组成的(Composition)。

同步

咱们在Reactor组合的状态通信协议中约定了同步通信,同时Reactor的Reaction过程也是同步的,这致使针对任何原始事件,整个Reactor Tree的Reaction是同步完成的。

要理解为何这个同步特性重要,咱们对比一下Process模型中的并发编程模型。

Process模型中定义的并发是指两个或两个以上Process在同时执行;基于Process模型并发编程只须要编写两个逻辑:Process的执行逻辑和Process之间的通信逻辑(ipc)。

Process的编程在代码形式上是同步的(blocking);Process之间的通信能够经过某种系统或run-time提供的ipc机制实现。

若是全部ipc都是同步的(blocking & unbuffered),这简化对Process之间交互逻辑的编程,就像一个transport层的传输协议使用了stop-and-wait(ack)方式实现,但效率上这是没法使用的;而异步实现ipc会让编写并发过程的交互逻辑显著困难。

在React模型中,React组合关系的Reaction,对应了Process模型中的ipc通信和Process之间的交互逻辑;在Reactor模型下,咱们为每一个过程创建了显式状态,定义了通信时的状态约定,Reactor能够同步得到成员的状态,能够同步的建立新过程、销毁正在执行的过程(强制状态迁移),那么Reactor之间的所有交互逻辑,均可以同步完成。这会大大简化这种逻辑的设计、实现、调试和测试。

咱们在前面的定义里看到了,Reactor和Process是不一样的概念,可是他们表示的都是过程;而并发编程的本质(和难点),就是在编程并发过程之间的交互和关系。在Reactor模型中的同步通信,能够给这种编程带来的显著收益。

这种同步方法的相关理论研究与实践,参见脚注4。

肯定性

Reactor模型下的并发编程,其系统行为具备肯定性:

  1. 在事件模型下,单线程的执行方式消除了任务调度致使的不肯定性;
  2. Reactor的同步状态迁移和通信消除了(异步)ipc通信致使的不肯定性;
  3. 虽然在任何一个时刻,整个系统没法预知下一个到来的事件会是谁?但不管是谁,咱们均可以得到当前状态+下一事件=下一状态意义上的肯定性。

在并发系统编程中,这种肯定性是宝贵的财富。

完备性

经典的状态机方法没法scale,由于软件的实际状态空间太大了,并且是动态的。

在数学上咱们解决无限(infinity)问题惟一的工具就是递归(recursion),与之等价的是概括法(induction)。

若是咱们定义了并发系统的初始状态,它的第一个事件到来,由于Reactor模型具备上述的肯定性,咱们能够获得下一个系统状态的定义;即便咱们没法确切的预知下一个事件是谁,理论上咱们能够考虑对全部可能的下一个事件,咱们均可以给出一个具备肯定性的总体状态迁移的设计;在这个过程当中应用概括法,咱们能够获得若是肯定知道系统在第N个事件处理结束后的状态,咱们能够给出它在第N+1个事件到来的肯定设计,这构成了在设计上的完备性。

Reactor模型中Reactor具备外部黑盒定义和组合方式保证了这种设计也是能够黑盒组合的,符合咱们在软件工程中构建系统时使用的方法论要求(Divide and Conquer, or Decomposition and Composition)。

缺点

Reactor在模型上的主要缺点是:和Process模型相比,它的reactive composition流程的书写,不如在Process内书写if/then/else流程语句来得方便;但它的Reaction具备同步特性,书写上要更加便利。

Process模型和Reactor模型,或者说Thread模型和Event模型,他们构成了Duality,前者在编写过程逻辑时简单,在编写过程交互逻辑时困难,后者正好相反;因此到底孰优孰劣,取决于须要编程的问题,和实际系统实现的种种限制。

在Node.js中使用Reactor模型编程,在计算任务上有限制。由于Node.js是单线程执行的,全部的Reaction逻辑都运行在一个CPU线程下,它有收益,可是在计算密集型任务时,会遇到性能瓶颈。

有这样一些解决方法:

  1. Node.js和JavaScript直接支持异步计算过程;实际执行能够把计算任务抛到其余线程去执行,但主线程仍然采用异步函数或EventEmitter的接口形式,即计算任务也是Reactor;这种方式应该是JavaScript的正确进化路线(而不是thread和lock),可是遥遥无期;它须要JavaScript在根本上支持immutable数据结构。
  2. 用native add-on实现异步计算;这是目前可用的方式,优势是效率,缺点是须要书写C/C++代码;Node.js中内置的部分crypto函数是这样实现的;
  3. 用子进程计算;ipc的效率是较大的问题,子进程的启动时间和内存消耗也是问题,适合某些使用场景但不是好的通用问题解决方案。

小结

至此咱们完整阐述完了Reactor模型,也看到了它和Process模型编程模式的差别与关系。

Reactor是动态对象,具备可组合特性,组合的Reactor Tree可描述整个应用的运行时状态。

Reactor的组合特性根本上改变了执行流程的构建方式,它在单一Reactor的Reaction代码书写上仍然使用代码控制流程,可是在Reaction级联时成为Reactive模式;整个应用都是Reactive的。

与Process模型相反,用Reactor为过程建模,过程间的通信与交互逻辑成为同步逻辑;这既是Reactor模型的特征,也是这种编程模式的最大收益。

Reactor模型理论上具备肯定性和设计完备性;实际应用中这须要严格遵循Reactor模型的行为、状态与状态通信协议的设计规范。

并发

咱们以并发做为命题和出发点,可是到目前位置并无深刻谈并发编程。

由于在Reactor模型下,咱们已经构建的概念和规则,构成了这个模型的语法(Syntax);可是并发问题,因为Reactor模型的同步特性,它演化成了一个纯粹的语义(Semantics)问题(或者说算法问题)。

在并发中有一些常见的概念,都是在Process模型下创建的,包括fork/join,race/settle,push/pull,并发控制和调度;另一些概念是属于并发编程模型自己的,不限因而Process模型仍是Reactor模型,例如responsive/latency/priority,starving,fairness等等。


这些概念在Reactor模型中能够这样简单陈述:

  1. fork: 建立多个新的Reactor;
  2. join: 把多个任务从已完成队列中移除,同时把新任务推入等待队列,或直接建立Reactor;
  3. race: 在Reactor的非finish事件中建立下一步逻辑的Reactor;
  4. settle: 在Reactor的finish事件中建立下一步逻辑的Reactor;
  5. push: 在Reactor完成时马上推入下一个任务队列;
  6. pull: 在Reactor完成时若是下一个任务队列已满,中止调度;等到下一个任务队列能够填充时向上一任务的已完成任务队列提取任务,而后用调度器推入执行队列;
  7. concurrency control: 限制并发成员的数量,使用等待队列;
  8. schedule:根据设计要求把等待队列中的任务推入执行队列;
  9. responsive:使用优先级和并发较少的执行队列;
  10. starving:使用bounded执行队列和控制任务的单次执行时间;
  11. fairness:全局调度;

好消息是全部这些问题能够用一把锤子解决:数学上的Petri Nets模型,它直接支持并发过程,和Reactor模型的结合美好到完美无缺。

坏消息是你可能会感到代码是外星人写的。可是他们仍然简单、易于理解、和易于调试。它的古怪不是模型带来的问题,而是Node.js中事件模型和异步过程的奇葩结合的结果。

但咱们仍然认为Node.js是天赋禀异的,由于不管代码能力多强,你永远不可能战胜数学;在数学上:

Concurrent System === Reactive System

Stay Tuned.

参考资料

一下参考资料中,3和4的部分章节是大力推荐想全面了解状态机方法如何应用到scalable的系统的读者阅读的。

[1] UML State Diagram,wikipedia entry

[2] David Harel,1984,paper, Statecharts: A Visual Formalism for Complex SystesmPDF download

[3] MIT OCW textbook,Ch4, State Machine,PDF download

[4] Edward A. Lee and Sanjit A. Seshia, 2017 book, Introduction to Embedded SystemsPDF download

[5] Albert Benveniste,1991,paper,The synchronous approach to reactive and real-time systemsPDF download

[6] Nicolas Halbwachs,1993,book,Synchronous Programming of Reactive SystemsPDF download

Footnotes

1 概念

在逻辑学家和语言学家那里是用一种近乎苛刻的方式理解“概念”一词的。

例如冯小刚是一我的的名字(name),这个名字所指的人,即冯小刚本人,称为这个名字的denotation (the thing denoted)。

可是咱们也能够用其余的名字指冯小刚这我的,例如“徐帆的老公”或“集结号的导演”,这两个名字是不一样的含义(sense),在逻辑学和语言学上把这个含义称为概念(concept)。

在这里通用的“过程”一词,“Process模型中的Process”,和“Reactor模型中的Reactor”,它们所指(denote)的事物都是同样的,可是它们的概念(concept)不一样,取决于上下文,即它和外部其余概念的关系。

2 Explicit State和Super State

一个Reactor的Explicit State是其内部实现的状态机中的Super State;这里Super State符合UML State Diagram的定义,其模型来自David Harel定义的Statecharts(Hierarchical State Machine)。

在Graph的意义上说,一个Reactor的Explicit State的状态迁移图是内部状态机的状态迁移图通过Graph Contraction操做以后获得的结果。

3 动态状态机

动态性在软件领域看起来是一个常识,过程和对象均可以动态建立和销毁;可是在硬件电路设计、控制系统,最小的嵌入式系统,和相似的状态机应用领域中并不常见。

经典状态机模型不是动态的,Harel设计的Hierarchical State Machine所提供的Hierarchy并非组件模型意义上的组合定义,它只是把复杂的Flat State Machine做抽象造成Hierarchical结构,是一种Graph变换,它没法scale。

经典状态机可以Scale的方式是经过io通信组合(compose)状态机单元,包括级联(cascading),并行(parallel),反馈(feedback)或任意有向图组合;这种状态机模型称io automata,它是能够scale的,也是人类可以设计出具备72亿晶体管且能够可靠工做的集成电路芯片的根本;可是它仍然是静态的,由于芯片上的硬件单元并无动态建立和销毁的可能,单元之间的通信线路也没法动态增长。

在io automata上继续拓展出动态能力的模型称dynamic io automata,这是相对而言在理论研究方面较新的领域,其形式化工做在最近两年才有相关的学术成果。

Reactor Model彻底符合dynamic io automata的定义。

4 同步方法

本文所述的Reactor模型中的同步反应和通信,在研究领域不是新课题。

最初的研究从法国Inria大学开始,文献5是我能找到的最先的文章,法国的研究者们(主要来自Inria大学)在这个领域作了普遍的工做,并设计了多种编程语言直接支持这种同步特性。文献6是对这些工做全面翔实的介绍。

在文献4中,做者把这种应用同步方法构成的系统称为Synchronous-Reactive Models,本文所述Reactor模型和书中所述SRM彻底一致。

Reactor模型的主要工做是在SRM基础上给出了组合过程时的通信协议约定,把SRM中的概念对应到Node.js的编程实践中,并用于解决实际的并发编程问题。

相关文章
相关标签/搜索