微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Reactor Model 1

Reactor Model是一种在Node.js中实现并发编程的模型,它是一个极为简单易用的概念模型,但仍然具有良好的特性和组合能力,从根本上解决并发和异步编程的困难。

Node.js不是唯一可以应用Reactor Model的语言,但是Reactor Model需要事件模型和用线程池实现的异步I/O

Reactor

一个Reactor是对一个异步过程的抽象。

一个Reactor具有如下特性:

  1. Reactor具有input和output;
  2. Reactor是异步的,这里异步的定义是对任何input而言,Reactor不得同步产生output
  3. Reactor是显式有态的;
  4. Reactor是动态的,可以被动态创建和销毁(destroy);

Reactor是一个概念模型,没有统一的代码形式约定;Node.js里几乎所有的异步API函数和对象都可以用Reactor来描述,虽然它们的行为不一定能100%遵循这里写的定义,尤其是后述的Reactor状态通讯协议,但通常可以很容易的再封装一下。

Reactor通常有两种代码形式:

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

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

例子1:异步函数

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

调用一个异步函数可以创建一个Reactor实例,或者说一个异步函数一个Reactor工厂(虽然不一定可以获得实例引用);在概念上,该实例在创建时就得到了input,在运行时通过callback函数输出结果。

异步函数构造的Reactor只有一个状态,记为S,它的结束状态记为0,所以这个Reactor的状态迁移可以简单的写成:

S -> 0

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

例子2:Spawn一个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方法是使用者需要的,返回函数对象也是可以的,但是如果未来要给这个对象增加方法就麻烦了。

const spawnChild2 = (command,() => {
    mute()
    callback(new Error('unexpected exit'))
  })

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

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

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

这是一个更为复杂的情况,使用者可能期望等待到子程序真正结束。S -> 0和前面一样,表示子程序可以直接结束;S => E表示子程序可能发生错误,或者被强制销毁,进入E状态,最终自发迁移到结束。

在这种情况下,需要使用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()
  }
}

和前面一样,这里仍然强调每状态使用独立的event handler原则。

例子3: stream

Node.js有极为易用的stream实现。

对Readable stream,通常S -> 0 | S => E -> 0可以描述其状态。在Node 8.x版本之后,有destroy方法可用。

对于Writable stream,S状态可能需要区分S0和S1,区别是S0是end方法尚未被调用的状态,S1是end方法已经被调用的状态。区分这两种状态的原因是:在使用者遇到错误时,它可能希望尚未end的Writable Stream需要抛弃,但已经end的Writable Stream可以等待其结束,不必destroy。

在这种情况下,严格的状态表述可以写成S0 -> S1 -> 0 | (S0 | S1) => E -> 0

Node.js里的对象设计和Reactor Model的设计要求高度相似,但不是完全一致。一般而言stream不需要再封装使用。实际上熟悉Reactor Model之后前面的spawn child也没有封装的必要,代码中稍微写一下是使用了哪个状态设计即可。

小结

这一节主要给出一个Reactor是什么的定义。它表示一个异步过程或者一个异步对象,绝大多数情况下它都是应该结束的,但是这也不是一个强制要求,例如一个Http Server或者用setInterval创建的一个时钟,可以永不结束。

在Reactor Model中Reactor是对异步的封装,它通过input和output与使用者通讯,并保证input和output是异步的,它是有态的和动态的。

实现一个Reactor应该尽可能提供destroy方法,并诚实汇报结束事件;否则经过组合后的粗粒度的Reactor将无法完成destroy和结束事件的实现。

Synchronous Communication

Reactor本身是异步的,但是Reactor的通讯是同步的。

同步在这里包含这样一些含义:

  1. 无论是内部事件导致的状态迁移,还是外部(使用者)强制的状态迁移,Reactor都必须同步完成;
  2. 内部事件导致的状态迁移,Reactor应该在完成状态迁移后,同步emit(output)事件;
  3. 外部强制的状态迁移,不允许同步或异步emit(output)事件;

前面一节列举的常见状态协议设计,已经可以涵盖日常开发中99%以上的Reactor状态协议设计,在这些协议设计下:

  1. 进入E状态应该emit error;
  2. 结束时应该emit finish,允许提供error作为参数;

在一次事件处理中连续两次或两次以上emit事件不是一个好的工程实践。最常见的情况是在emit error之后同步emit finish;在这种情况下应该只emit finish。

emit事件一定要在状态迁移完成后进行,在状态迁移过程中emit事件是危险的;因为在状态迁移过程中Reactor的内部状态可能不完整,如果使用者在handler中调用方法容易发生错误

Compositionality

组合特性

组合特性是最重要但却最常见被忽视的软件构建要求。

一个模型中的构建可以组合,指的不只是能够把相对简单的构件组装成相对复杂的构件,它还要求如果那些相对简单的构件具有某些特性,这些特性应该在组装后的复杂构件上得以保留

我们可以观察一下数学家如何构建一个数学系统。比如在有了1和2这两个数字和加法的定义之后,数学家可以加入3这个数字,1和2是可以相加的,1和2组成的3仍然是可以相加的,在往整数这个系统中不断添加新的数字时我们需要保证已经定义的运算仍然可用,在整数拓展到有理数、无理数、复数之后,原有的运算都尽可能得到保留,同样的当引入一种新的运算时,例如三角函数,它也可以应用到尽可能的多的已有的数上。在这个游戏不断进行下去之后,我们得到一个数量上和特性上都丰富的系统,它就会变得非常易用也非常Powerful。

在编程语言中,我们看到函数可以组合成函数,组合函数仍然具有函数特征:它有输入输出和可以执行;对象可以组合成对象,组合的对象仍然是对象,它可以有引用有成员变量有方法可用。

一个例子,在Node.js中为什么callback被认为使用不变,代码缩进不是主要原因,而是传统的if/then等流程控制逻辑书写困难,或者说这个形式失去了直接应用流程语句的特性。

Reactor组合

前面看到的代码例子实际上已经是在组合Reactor的代码形式,只是每个Reactor仅有一个成员,所以只能称为简单封装。后面的代码例子会展示更复杂的情况,尤其是并发组合过程。

Reactor Model不需要使用任何基础类或Library代码实现,例如象React库那样提供React.Component作为基础类;但是应用Reactor Model需要开发者严格遵守上面的行为定义和通讯协议要求,保证每一个Reactor都满足异步过程和同步通讯的要求。只有要求被满足,这个组合过程才能持续下去,在各个粒度上都能保留可以继续组合的能力,方便代码重用和易于应对设计变更。

Why Reactor Model?

Duality

并发一词通常是在Process模型中定义的。一个能被称为Process的对象,唯一需要的特性是它是可执行的。

基于Process模型并发编程只需要编写两个逻辑:Process的执行逻辑和Process之间的Interaction逻辑。

如果系统的实现也是基于Process模型实现的,一个Process可以用任何可执行的方式执行,包括进程、线程、协程等等;Process之间的Interaction,可以通过某种ipc机制实现。

如果所有ipc都是同步的(blocking & unbuffered),这会简化编程,就像一个transport层的传输协议使用了stop-and-wait(ack)方式实现,但效率上无法使用;但异步实现会让编写并发过程Interaction的逻辑显著困难。

在React Model中,处理并发过程间interaction逻辑的代码一个同步过程;任何一个基础事件到来后,它从(Hierarchical) Reactor Tree的Leaf Node开始触发Event Handler,在不断向上populate的过程中也可以向下populate(但不会循环因为Reactor有异步保证),所有Reactor的状态是可以同步获得的,对Reactor的操作,包括创建和销毁Reactor、改变Reactor的执行路径,都是同步执行的。

在这里我们看到了应对并发编程,在Process Model和Reactor Model(或者说Thread vs Event)中完全对立的两个做法:在Process Model中,编写Process采用同步的方式,编写Process Interaction采用了异步的方式;但是在Event Model中,Process成了异步过程,但是Process Interaction是同步过程。

Reactive

在Reactor Model中,上述同步过程的执行流程是通过Reactor组合级联出来的,Reactor Model中的状态通讯协议让这个级联出来的执行路径是相对容易设计、实现、调试,并且容易做到可靠的;当然这种执行流程的设计和实现,与Process模型中用流程控制语句来设计和实现执行流程相比,是不够简明紧凑的,但这种差异正体现Reactive系统编程的特点。

一个函数里,使用流程控制语句书写一段程序是简单直观的,但是如果我们问这样一个问题:如果在执行过程中,这个函数域内的变量在不断的发生自发变化(volatile),这个过程的书写还会简单吗?难道这时不该把模型改成状态机?

从这个意义上说,如果需要系统逻辑是可以fine-grained,容易在最细的粒度上应对系统的设计变更、性能调优、通过heuristic算法精细为并发任务分配资源,达到在各种极限使用场景下系统的高可用性和服务的fairness,牺牲Process Model下熟悉且简单的流程控制逻辑不可避免

控制(Control)和反应(Reactive)是完全相反的设计逻辑,而Concurrent System === Reactive System

这也是选择Reactor一词作为这个模型中的基础构件的名称的原因。

Determinism

Reactor Model下的并发编程,其系统行为是容易理解的,因为:

  1. 在事件模型下,单线程的执行方式消除了任务调度导致的不确定性;
  2. Reactor的同步状态迁移和通讯消除了(异步)ipc通讯导致的不确定性;
  3. 虽然在任何一个时刻,整个系统无法预知下一个到来的事件会是谁?是一个新的外部请求还是一个系统自己发出的网络或文件系统I/O请求的返回,但是无论是哪一个,我们都可以获得当前状态+下一事件=下一状态意义上的确定性。

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...