国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

白潔血戰Node.js并發編程 01 狀態機

fjcgreat / 2543人閱讀

摘要:狀態機狀態機是模型層面的概念,與編程語言無關。狀態機具有良好的可實現性和可測試性。在代碼里,這是一個,但是我們在狀態機模型中要把他理解為事件。

這一篇是這個系列的開篇,沒有任何高級內容,就講講狀態機。

狀態機

狀態機是模型層面的概念,與編程語言無關。它的目的是為對象行為建模,屬于設計范疇。它的基礎概念是狀態(state)和事件(event)。

對象的內部結構描述為一組狀態S1, S2, ... Sn,它的行為的trigger,包括內部的和外部的,描述成為一組事件E1, E2, ... En,在任何狀態下,任何事件到來,對象狀態的改變用Sx -> Sy的狀態遷移(State Transition)來描述,這個狀態遷移就是對象的行為(behavior)。

對對象行為的完備定義就是{ S } x { E }的矩陣,如果存在(Sx, Ey)的組合未定義行為,這個對象行為模型在設計層面上就不完備,當然實際的代碼不可能沒有行為,這往往就是錯誤發生的地方。

狀態機具有良好的可實現性和可測試性。完備定義的狀態機很容易寫出對應的代碼,也很容易遍歷全部的狀態遷移過程完成測試,當然這只意味著代碼實現和設計(模型)相符,并不意味著設計是正確的。

設計的正確性是一個復雜的多的話題,嚴格的定義是設計符合Specification。什么是符合Specification?要去看Robin Milner, Tony Hoare, Leslie Lamport等人的書了,老實說我也不懂,所以就此打住。

這篇文章不會詳細介紹狀態機,網上有非常多的資料,四人幫的書上有State Pattern - OO語言下的狀態機實現,UML有State Diagram,是非常好的圖示工具;這里只給出一個代碼例子,對照這個實例幫助理解狀態機模型的代碼實現。

一個例子

假定我們要解決這樣一個任務:我們有一個模塊是為了存儲(save)一個文件,寫狀態機的目的是為了解決并發操作時排隊存儲的請求,因為請求是并發的,如果寫入文件的io操作也是并發的話,這個文件可能被損壞。這是一個非常常見的應用場景。

這個模塊定義有三種狀態:

Idle, 這是不工作的狀態;

Pending,這是等待工作的狀態,等待的目的是為了,如果在很短的時間內出現連續多次的寫入請求,我們只寫入最后一個,減少io操作的次數;

Working,該狀態下在執行寫入操作,如果在執行io操作的時候收到寫入請求,我們把請求內容保存在一個臨時的位置;

Idle狀態沒有任何特殊資源,只有一個save請求事件;當有save請求時,它遷移到Pending狀態。

Pending狀態具有的特殊資源是一個timer,它可能有兩個事件:來自外部的save請求,和來自內部的timeout。在JavaScript代碼里,這是一個callback,但是我們在狀態機模型中要把他理解為事件。在Pending狀態中如果有save請求,不發生狀態遷移,新的請求數據會覆蓋舊的版本,原來的timer會被清除,重新開始新的timer。在timeout發生時,遷移到Working狀態。

Working狀態在進入時會啟動存儲文件的操作,它可能有兩個事件:來自外部的save請求,和來自內部的保存文件操作的異步返回,同樣的,它也被理解為一個(內部)事件。當外部的save請求到來時,該請求被保存在內部的next變量里;當文件操作返回時:

如果操作成功

如果存在next,向Pending狀態遷移

如果不存在next,向Idle狀態遷移

如果操作失敗

如果存在next,向Pending狀態遷移,用next作為數據

如果不存在next,也向Pending狀態遷移,仍然使用當前數據,相當于等待后retry

我偷個懶,沒給出圖示,實際上這樣的語言描述不如State Diagram來得直觀。下面的表格是上述語言描述的歸納,史稱狀態遷移表(State Transition Table),有了State Diagram或者State Transition Table,任何人寫出來的代碼都一樣。為了表述清晰,表中把Working狀態的文件操作返回分成了兩個事件:success和error。

StateEvent Save Timeout Success Error
Idle -> Pending n/a n/a n/a
Pending overwrite data, restart timer -> Working n/a n/a
Working set next n/a if next, -> Pending; else -> Idle -> Pending(next ? next : data)
代碼

下面是代碼,首先有個base class,三個狀態都繼承自這個base class:

class State {

  constructor(ctx) {
    this.ctx = ctx
  }

  setState(nextState, ...args) {
    this.exit()
    this.ctx.state = new nextState(this.ctx, ...args)
  }

  exit() {}
}

在狀態機的代碼實現上,標志性的方法名稱是setState,它負責狀態遷移;其次是enterexit,分別對應進入該狀態和離開該狀態。

狀態機模式(State Pattern)的一個顯著的編程收益是:每個狀態都有自己的資源,在遷入該狀態的時候建立資源,在離開該狀態的時候釋放資源,這很容易保證資源的正確使用。

在上述代碼中,constructor充當了enter邏輯的角色,所以沒有提供獨立的enter方法;JavaScript Class是一個語法糖,沒有和constructor相對應的destructor,所以我們這里寫一個exit函數,如果繼承類里沒有exit邏輯,這個基類上的方法就是一個fallback。

ctx是一個外部容器,相當于所有狀態對象的上下文(context),它同時具有一個叫做state的成員,該成員是一個Idle,Pending,或者Working類的實例;無論ctx.state是哪個狀態,ctx都把save方法forward到state上,這樣寫是一個很標準的State Pattern。

setState的實現有點tricky,是JavaScript的特色。它首先調用當前類的exit函數遷出狀態,然后使用new來構造下一個類,這意味著第一個參數nextState是一個構造函數;后面的參數使用了spread operator,把這些參數傳入了構造函數,同時把新對象安裝到了ctx,即把自己替換了;這不是唯一的做法,是比較簡潔的一種寫法。

Idle類的實現非常簡單,在save的時候用data作為參數構造了Pending對象。

class Idle extends State{

  save(data) {
    this.setState(Pending, data)
  }
}

Pending類的save方法里保存了data和啟動timer。它的構造函數重用了save方法。因為JavaScript的clearTimeout方法是安全的,這樣寫沒什么問題。

exit函數實際上沒有必要,但這樣書寫是推薦的,它確保資源清理,如果未來設計變更出現其他的狀態遷出邏輯,這個代碼就有用了。

timeout時PendingWorking狀態遷移。

class Pending extends State {

  constructor(ctx, data) {
    super(ctx)
    this.save(data)
  }

  save(data) {
    clearTimeout(this.timer)
    this.data = data 
    this.timer = setTimeout(() => {
      this.setState(Working, this.data) 
    }, this.ctx.delay)
  }

  exit() {
    clearTimeout(this.timer)
  }
}

Working代碼稍微多點,但是對照狀態遷移表很容易讀懂。不贅述每個方法了。保存文件的操作采用了先寫入臨時文件然后重命名的做法,這是保證文件完整性的常規做法,系統即使斷電也不會損壞磁盤文件。

class Working extends State {

  constructor(ctx, data) { 
    super(ctx)
    this.data = data 

    // console.log("start saving data", data)
    let tmpfile = path.join(this.ctx.tmpdir, UUID.v4())
    fs.writeFile(tmpfile, JSON.stringify(this.data), err => {

      if (err) return this.error(err)
      fs.rename(tmpfile, this.ctx.target, err => {

        // console.log("finished saving data", data, err)
        if (err) return this.error(err)
        this.success()
      }) 
    })
  } 

  error(e) {
    // console.log("error writing persistent file", e)
    if (this.next)    
      this.setState(Pending, this.next)
    else
      this.setState(Pending, this.data)
  }

  success() {
    if (this.next)
      this.setState(Pending, this.next)
    else 
      this.setState(Idle)
  }

  save(data) {
    // console.log("Working save", data)
    this.next = data
  }
}

最后是ctx,我們在實際項目中稱之為Persistence。它初始化時設置state為Idle狀態;把所有的save操作都forward到內部對象state上。

class Persistence {

  constructor(target, tmpdir, delay) {
    this.target = target 
    this.tmpdir = tmpdir
    this.delay = delay || 500
    this.state = new Idle(this) 
  }

  save(data) {
    this.state.save(data)
  }
}
要點

這一篇粗略的講了兩個問題:狀態機模型和狀態機模式(State Pattern)。他們兩個不是一回事。

狀態機模式是一種寫法,上述寫法不唯一;不使用Class,僅僅在Persistence類中使用(枚舉)變量表示狀態也是可以的,使用Class則相當于用變量類型來代表狀態。

狀態機模式的顯著優點在于:

不同狀態的資源和行為邏輯分開

setState, enter, exit等標志性方法中不需要使用if / then或switch語句

在對象行為定義發生變化時,修改容易,不易犯錯誤;感謝enter和exit的封裝,它強制了資源回收邏輯

狀態機模型的意義對后面的內容更為重要。上面的例子具有這樣幾個特征:

狀態具有顯式定義

事件內外有別

外部事件對所有狀態成立,因此Persistence類的使用非常簡單,從外部其實看不到內部有什么狀態定義,黑盒意義上說,Persistence是無態的,這對使用便利性來說極為重要;

內部事件僅僅對某些狀態成立,所有異步函數的返回都理解為事件,而且是唯一的內部事件;

從并發角度說,Persistence類是一個同步器(Synchronizer),即并發的save操作在這里被排序執行了;當然我們沒有設計更復雜的邏輯,例如任務隊列,但顯然那不是很難;

問題

純粹的狀態機(automata)對于并發編程是無力的,這是一種共識,因為并發帶來的狀態組合會迅速爆炸狀態空間,我們要找到辦法對付這個問題,此其一。

其二,實際的程序模塊組合時常見包含關系,用經典的狀態機模型會產生組合狀態機(Hierarchical State Machine),它的代碼書寫遠比上述例子的Flat State Machine難寫,除非在語言一級或者有類庫支持,否則可讀性和可維護性都很差,設計變更時代碼改動幅度非常大,不是解決常見問題的好辦法,雖然在一些特殊應用領域卓有建樹,例如嵌入式設備的通訊協議棧。

事件(Event)和線程(Thread)是形式上對立,但是數學上對等,的兩個編程方式。兩者各有利弊,戰爭也是古老的,你在網絡上很容易搜索到Why Event (Model) is Evil或者Why Thread (Model) is Evil的學術文章,都有大量的支持者。

Node.js的與眾不同之處在于它的強制non-blocking i/o設計。這給習慣Thread編程的開發者制造了麻煩,所以在過去的幾年里新的過程原語被發明出來解決這個問題,包括promise,generator,async/await。bluebird的使用者越來越多,而caolan的曾經很流行的async庫用戶越來越少。

但是眾所周知JavaScript語言是事件模型的。在基礎特性上尋求類thread編程形式去解決一切問題本身就是表里不一的,而且promise和async/await的實現本身也有很多不盡人意的地方。

這讓我們倒回來思考兩個問題:

尋求各種CPS(Continuation Passing Style)是解決non-blocking i/o的必經之路嗎?

事件和狀態機模型真的沒有辦法寫規?;牟l程序嗎?

Node原作者Ryan Dahl最近在一次訪談里說了他對Node的看法。他說在最初的兩三年中他是狂熱的支持Node的強制non-blocking i/o設計的,達到那種認為“原來我們都做錯了”的程度,但是慢慢的他的態度發生了轉變,尤其是在接觸了Go語言之后;現在他的看法是,最初他以為Node可以做到是end-all或者for-all的,但是現在他沒那么有信心了,在并發編程上他認為Go可能是更好的設計。

我的個人觀點,談Node必談callback hell的開發者,并不熟悉在Event Model下的并發編程技術,promise和async/await本質上,絕大多數情況下是在serialize過程,如果只是serialize,那么結果和blocking i/o的編程并不會有區別。Promise對parallel的支持很有限,它只是在serial的過程序列上偶爾撒一點parallel的flavor。而且如果你喜歡的就是Thread Model,那么就應該選擇對它有良好支持的編程語言或環境,例如Go或者fibjs。

如果你像我一樣,喜歡JavaScript語言的簡單,喜歡Event Model的簡單,而不只是因為Node有良好的生態圈和海量的npm包可用而選擇了Node——如果你只是因為這兩點選擇了Node,你肯定會后悔的——那么擺在我們面前的問題就是:事件模型,顯式狀態,non-blocking i/o,我們能不能找到一種辦法,一種end-allfor-all的辦法,最好能夠直接體現在代碼形式上,或者至少體現在一個簡單、直覺、不易錯、同時保持經典狀態機模型的完備性的Mental Model上,能夠為復雜的并發編程問題建模和書寫代碼?

在這里經典狀態機模式可以給我們一個簡單啟迪:我們不僅可以用來表示狀態,我們也可以用對象類型表示狀態,而且有明顯的收益。同樣的,在事件模型下解決并發問題的關鍵,就是把這個設計繼續向前推進一步,我們還可以用結構來表示狀態。具體怎么寫和怎么思考建模,則是這個系列文章的主旨。

這在數學層面上非常容易理解:所謂并發編程,它就是在structure過程(Rob Pike)。函數或者類函數,包括promise,async function,generator,coroutine,他們是Thread Model下的(黑盒)原語和原語組合,對應的,我們要找到事件模型下的顯式狀態方法來應對這個問題,如果能做到這一點,我們就可以回到純粹的事件模型下編寫程序。

這個結果并不難,但是,它也確實有一段路要走,我們需要仔細梳理過程原語的優點缺點,梳理并發編程的本質,梳理常見問題的各種編程方式,最后回到我們的事件模型和狀態機上來。等這個系列寫完,你也讀完,我向你保證,你再次看到callback函數時會覺得原來它那么簡單且美。

下一篇我們開始談并發編程,敬請期待。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88452.html

相關文章

  • 白潔血戰Node并發編程 - 預覽

    摘要:在這些動作結束后,所有的隊列變化,就是整個組合任務狀態機的下一個狀態。如果組合狀態機停止了,向其中的任何一個對象執行或者操作都可以讓整個狀態機繼續動起來。 預覽。 先給出一個基礎類代碼。 const EventEmitter = require(events) const debug = require(debug)(transform) class Transform extend...

    Lemon_95 評論0 收藏0
  • 2017-08-10 前端日報

    摘要:前端日報精選譯用搭建探索生命周期中的匿名遞歸瀏覽器端機器智能框架深入理解筆記和屬性中文上海線下活動前端工程化架構實踐滬江技術沙龍掘金周二放送追加視頻知乎專欄第期聊一聊前端自動化測試上雙關語來自前端的小段子,你看得懂嗎眾成翻 2017-08-10 前端日報 精選 [譯] 用 Node.js 搭建 API Gateway探索 Service Worker 「生命周期」JavaScript ...

    wupengyu 評論0 收藏0
  • 和少婦白潔一起學JavaScript之Async/Await

    摘要:匿名函數是我們喜歡的一個重要原因,也是,它們分別消除了很多代碼細節上需要命名變量名或函數名的需要。這個匿名函數內,有更多的操作,根據的結果針對目錄和文件做了不同處理,而且有遞歸。 能和微博上的 @響馬 (fibjs作者)掰扯這個問題是我的榮幸。 事情緣起于知乎上的一個熱貼,諸神都發表了意見: https://www.zhihu.com/questio... 這一篇不是要說明白什么是as...

    Bryan 評論0 收藏0
  • 2017-10-05 前端日報

    摘要:前端日報精選瀏覽器渲染過程與性能優化新版采用新引擎,速度是舊版的倍,名字和也變了中文與的使用個人文章在對比中理解掘金白潔血戰并發編程異步英文 2017-10-05 前端日報 精選 瀏覽器渲染過程與性能優化Firefox 新版采用新引擎,速度是舊版的 2 倍,名字和 Logo 也變了8 Key React Component DecisionsThe Intl.PluralRules A...

    tracy 評論0 收藏0
  • 和少婦白潔一起學JavaScript之Async/Await II

    摘要:的科學定義是或者,它的標志性原語是。能解決一類對語言的實現來說特別無力的狀態機模型流程即狀態。容易實現是需要和的一個重要原因。 前面寫了一篇,寫的很粗,這篇講講一些細節。實際上Fiber/Coroutine vs Async/Await之爭不是一個簡單的continuation如何實現的問題,而是兩個完全不同的problem和solution domain。 Event Model 我...

    番茄西紅柿 評論0 收藏0

發表評論

0條評論

fjcgreat

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<