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的編程實踐中,並用於解決實際的併發編程問題。

相關文章
相關標籤/搜索