ES6 Generators併發

  ES6 Generators系列:

  1. ES6 Generators基本概念
  2. 深刻研究ES6 Generators
  3. ES6 Generators的異步應用
  4. ES6 Generators併發

  若是你已經讀過這個系列的前三篇文章,那麼你確定對ES6 generators很是瞭解了。但願你能從中有所收穫並讓generator發揮它真正的做用。最後咱們要探討的這個主題可能會讓你血脈噴張,讓你絞盡腦汁(說實話,寫這篇文章讓我很費腦子)。花點時間看下文章中的這些例子,相信對你仍是頗有幫助的。在學習上的投資會讓你未來受益無窮。我徹底相信,在將來,JS中那些複雜的異步能力將起源於我這裏的一些想法。html

 

CSP(Communicating Sequential Processes)

  首先,我寫這一系列文章徹底是受Nolen @swannodette出色工做的啓發。說真的,他寫的全部文章都值得去讀一讀。我這裏有一些連接能夠分享給你:node

  好了,讓咱們正式開始對這個主題的探討。我不是一個從具備Clojure(Clojure是一種運行在Java平臺上的 Lisp 方言)背景轉投到JS陣營的程序員,並且我也沒有任何Go或者ClojureScript的經驗。我發現本身在讀這些文章的時候很快就會失去興趣,所以我不得不作不少的實驗並從中瞭解到一些有用的東西。git

  在這個過程當中,我以爲我已經有了一些相同的思想,並追求一樣的目標,而這些都源自於一個不那麼古板的思惟方式。程序員

  我嘗試建立了一個更簡單的Go風格的CSP(以及ClojureScript core.async)APIs,同時我但願能保留大部分的底層功能。也許有大神會看到我文章中遺漏的地方,這徹底有可能。若是真是這樣的話,我但願個人探索可以獲得進一步的發展和演變,而我也將和你們一塊兒來分享這個過程!es6

 

詳解CSP原理(一點點)

  到底什麼是CSP?說它是"communicating","Sequential","processes"究竟是什麼意思呢?github

  首先,CSP一詞源自於Tony Hoare所著的「Communicating Sequential Processes」一書。裏面全是有關CS的理論,若是你對學術方面的東西感興趣的話,這本書絕對值得一讀。我決不打算以一種讓人難以理解的,深奧的,計算機科學的方式來闡述這個主題,而是會以一種輕鬆的非正式的方式來進行。編程

  那咱們就從"Sequential"開始吧!這部分你應該已經很熟悉了。這是另一種談論有關單線程和ES6 generators異步風格代碼的方式。咱們來回憶一下generators的語法:數組

function *main() {
    var x = yield 1;
    var y = yield x;
    var z = yield (y * 2);
}

  上面代碼中的每一條語句都會按順序一個一個地執行。Yield關鍵字標明瞭代碼中被阻塞的點(只能被generator函數本身阻塞,外部代碼不能阻塞generator函數的執行),可是不會改變*main()函數中代碼的執行順序。這段代碼很簡單!promise

  接下來咱們來討論一下"processes"。這個是什麼呢?數據結構

  基本上,generator函數有點像一個虛擬的"process",它是咱們程序的一個獨立的部分,若是JavaScript容許,它徹底能夠與程序的其它部分並行執行。這聽起來彷佛有點兒荒唐!若是generator函數訪問共享內存(即,若是它訪問除了本身內部定義的局部變量以外的「自由變量」),那麼它就不是一個獨立的部分。如今咱們假設有一個不訪問外部變量的generator函數(在FP(Functional Programming函數式編程)的理論中咱們將它稱之爲一個"combinator"),所以從理論上來講它能夠在本身的process中運行,或者說做爲本身的process來運行。

  可是咱們說的是"processes",注意這個單詞用的是複數,這是由於會存在兩個或多個process在同一時間運行。換句話說,兩個或多個generators函數會被放到一塊兒來協同工做,一般是爲了完成一項較大的任務。

  爲何要用多個單獨的generator函數,而不是把它們都放到一個generator函數裏呢?一個最重要的緣由就是:功能和關注點的分離。對於一個任務XYZ來講,若是你將它分解成子任務X,Y和Z,那麼在每一個子任務本身的generator函數中來實現功能將會使代碼更容易理解和維護。這和將函數XYZ()拆分紅X()Y(),和Z(),而後在X()中調用Y(),在Y()中調用Z()是同樣的道理。咱們將函數分解成一個個獨立的子函數,下降代碼的耦合度,從而使程序更加容易維護。

對於多個generators函數來講咱們也能夠作到這一點

  這就要說到"communicating"了。這個又是什麼呢?就是合做。若是咱們將多個generators函數放在一些協同工做,它們彼此之間須要一個通訊信道(不只僅是訪問共享的做用域,而是一個真正的能夠被它們訪問的獨佔式共享通訊信道)。這個通訊信道是什麼呢?無論你發送什麼內容(數字,字符串等),事實上你都不須要經過信道發送消息來進行通訊。通訊會像合做那樣簡單,就像將程序的控制權從一個地方轉移到另一個地方。

  爲何須要轉移控制?這主要是由於JS是單線程的,意思是說在任意給定的一個時間片斷內只會有一個程序在運行,而其它程序都處在暫停狀態。也就是說其它程序都處在它們各自任務的中間狀態,不過只是被暫停執行,必要時會恢復並繼續運行。

  任意獨立的"processes"之間能夠神奇地進行通訊和合做,這聽起來有點不靠譜。這種解耦的想法是好的,可是有點不切實際。相反,彷佛任何一個成功的CSP的實現都是對那些問題領域中已存在的、衆所周知的邏輯集的有意分解,其中每一個部分都被特殊設計過從而使得各部分之間都能良好工做。

  或許個人理解徹底是錯的,可是我尚未看到任何一個切實可行的方法,可以讓兩個隨機給定的generator函數能夠以某種方式輕易地聚合在一塊兒造成CSP對。它們都須要被設計成可以與其它部分一塊兒工做,須要遵守彼此間的通訊協議等等。

 

JS中的CSP

  在將CSP的理論應用到JS中,有一些很是有趣的探索。前面提到的David Nolen,他有幾個頗有趣的項目,包括Om,以及core.asyncKoa庫(node.js)主要經過它的use(..)方法體現了這一點。而另一個對core.async/Go CSP API十分忠實的庫是js-csp

  你確實應該去看看這些偉大的項目,看看其中的各類方法和例子,瞭解它們是如何在JS中實現CSP的。

 

異步的runner(..):設計CSP

  由於我一直在努力探索將並行的CSP模式應用到我本身的JS代碼中,因此對於使用CSP來擴展我本身的異步流程控制庫asynquence來講就是一件瓜熟蒂落的事。我寫過的runner(..)插件(看上一篇文章:ES6 Generators的異步應用)就是用來處理generators函數的異步運行的,我發現它能夠很容易被擴展用來處理多generators函數在同一時間運行,就像CSP的方式那樣

  我要解決的第一個設計問題是:如何才能知道哪一個generator函數將得到下一個控制權?

  要解決各個generators函數之間的消息或控制權的傳遞,每一個generator函數都必須擁有一個能讓其它generators函數知道的ID,這看起來彷佛過於笨拙。通過各類嘗試,我設定了一個簡單的循環調度方法。若是你匹配了三個generators函數A,B和C,那麼A將先得到控制權,當A yield時B將接管A的控制權,而後當B yield時C將接管B,而後又是A,以此類推。

  可是如何才能實際轉移generator函數的控制權呢?應該有一個顯式的API嗎?我再次進行了各類嘗試,而後設定了一個更加隱式的方法,看起來和Koa有點相似(徹底是之外):每一個generator函數都得到一個共享"token"的引用,當yield時就表示要將控制權進行轉移。

  另外一個問題是消息通道應該長什麼樣。一種是很是正式的通訊API如core.async和js-csp(put(..)take(..))。可是在我通過各類嘗試以後,我比較傾向於另外一種不太正式的方法(甚至都談不上API,而只是一個共享的數據結構,例如數組),它看起來彷佛是比較靠譜的。

  我決定使用數組(稱之爲消息),你能夠根據須要決定如何填充和清空數組的內容。你能夠push()消息到數組中,從數組中pop()消息,按照約定將不一樣的消息存放到數組中特定的位置,並在這些位置存放更復雜的數據結構等。

  個人疑惑是有些任務須要傳遞簡單的消息,而有些則須要傳遞複雜的消息,所以不要在一些簡單的狀況下強制這種複雜度,我選擇不拘泥於消息通道的形式而使用數組(除數組自己外這裏沒有任何API)。在某些狀況下它很容易在額外的形式上對消息傳遞機制進行分層,這對咱們來講頗有用(參見下面的狀態機示例)。

  最終,我發現這些generator "processes"仍然得益於那些獨立的generators可使用的異步功能。也就是說,若是不yield控制token,而yield一個Promise(或者一個異步隊列),則runner(..)的確會暫停以等待返回值,但不會轉移控制權,它會將結果返回給當前的process(generator)而保留控制權。

  最後一點也許是最有爭議或與本文中其它庫差異最大的(若是我解釋正確的話)。也許真正的CSP對這些方法不屑一顧,可是我發現個人選擇仍是頗有用的。

 

一個愚蠢的FooBar示例

  好了,理論的東西講得差很少了。咱們來看看具體的代碼:

// 注意:爲了簡潔,省略了虛構的`multBy20(..)`和`addTo2(..)`異步數學函數

function *foo(token) {
    // 從通道的頂部獲取消息
    var value = token.messages.pop(); // 2

    // 將另外一個消息存入通道
    // `multBy20(..)`是一個promise-generating函數,它會延遲返回給定值乘以`20`的計算結果
    token.messages.push( yield multBy20( value ) );

    // 轉移控制權
    yield token;

    // 從CSP運行中的最後的消息
    yield "meaning of life: " + token.messages[0];
}

function *bar(token) {
    // 從通道的頂部獲取消息
    var value = token.messages.pop(); // 40

    // 將另外一個消息存入通道
    // `addTo2(..)` 是一個promise-generating函數,它會延遲返回給定值加上`2`的計算結果
    token.messages.push( yield addTo2( value ) );

    // 轉移控制權
    yield token;
}

  上面的代碼中有兩個generator "processes",*foo()*bar()。它們都接收並處理一個令牌(固然,若是你願意你能夠隨意叫什麼都行)。令牌上的屬性messages就是咱們的共享消息通道,當CSP運行時它會獲取初始化傳入的消息值進行填充(後面會講到)。

  yield token顯式地將控制權轉移到「下一個」generator函數(循環順序)。可是,yield multBy20(value)yield addTo2(value)都是yield一個promises(從這兩個虛構的延遲計算函數中返回的),這表示generator函數此時是處於暫停狀態直到promise完成。一旦promise完成,當前處於控制中的generator函數會恢復並繼續運行。

  不管最終yield會返回什麼,上面的例子中yield返回的是一個表達式,都表示咱們的CSP運行完成的消息(見下文)。

  如今咱們有兩個CSP process generators,咱們來看看如何運行它們?使用asynquence:

// 開始一個sequence,初始message的值是2
ASQ( 2 )

// 將兩個CSP processes進行配對一塊兒運行
.runner(
    foo,
    bar
)

// 不管接收到的message是什麼,都將它傳入sequence中的下一步
.val( function(msg){
    console.log( msg ); // 最終返回42
} );

  這只是一個很簡單的例子,但我以爲它能很好地用來解釋上面的這些概念。你能夠嘗試一下(試着改變一些值),這有助於你理解這些概念並本身動手編寫代碼!

 

另外一個例子Toy Demo

  讓咱們來看一個經典的CSP例子,但只是從咱們目前已有的一些簡單的發現開始,而不是從咱們一般所說的純粹學術的角度來展開討論。

  Ping-pong。一個頗有趣的遊戲,對嗎?也是我最喜歡的運動。

  讓咱們來想象一下你已經完成了這個乒乓球遊戲的代碼,你經過一個循環來運行遊戲,而後有兩部分代碼(例如在ifswitch語句中的分支),每一部分表明一個對應的玩家。代碼運行正常,你的遊戲運行起來就像是一個乒乓球冠軍!

  可是按照咱們上面討論過的,CSP在這裏起到了什麼樣的做用呢?就是功能和關注點的分離。那麼具體到咱們的乒乓球遊戲中,這個分離指的就是兩個不一樣的玩家

  那麼,咱們能夠在一個很是高的層面上用兩個"processes"(generators)來模擬咱們的遊戲,每一個玩家一個"process"。當咱們實現代碼細節的時候,咱們會發如今兩個玩家之家存在控制的切換,咱們稱之爲"glue code"(膠水代碼(譯:在計算機編程領域,膠水代碼也叫粘合代碼,用途是粘合那些可能不兼容的代碼。可使用與膠合在一塊兒的代碼相同的語言編寫,也能夠用單獨的膠水語言編寫。膠水代碼不實現程序要求的任何功能,它一般出如今代碼中,使現有的庫或者程序在外部函數接口(如Java本地接口)中進行互操做。膠水代碼在快速原型開發環境中很是高效,可讓幾個組件被快速集成到單個語言或者框架中。)),這個任務自己可能須要第三個generator的代碼,咱們能夠將它模擬成遊戲的裁判

  咱們打算跳過各類特定領域的問題,如計分、遊戲機制、物理原理、遊戲策略、人工智能、操做控制等。這裏咱們惟一須要關心的部分就是模擬打乒乓球的往復過程(這實際上也表明了咱們CSP的控制轉移)。

  想看demo的話能夠在這裏運行(注意:在支持ES6 JavaScript的最新版的FireFox nightly或Chrome中查看generators是如何工做的)。如今,讓咱們一塊兒來看看代碼。首先,來看看asynquence sequence長什麼樣?

ASQ(
    ["ping","pong"], // 玩家姓名
    { hits: 0 } //
)
.runner(
    referee,
    player,
    player
)
.val( function(msg){
    message( "referee", msg );

  咱們初始化了一個messages sequence:["ping", "pong"]{hits: 0}。一下子會用到。而後,咱們設置了一個包含3個processes運行的CSP(相互協同工做):一個*referee()和兩個*player()實例。在遊戲結束時最終的message會被傳遞給sequence中的下一步,做爲referee的輸出message。下面是referee的實現代碼:

function *referee(table){
    var alarm = false;

    // referee經過秒錶(10秒)爲遊戲設置了一個計時器
    setTimeout( function(){ alarm = true; }, 10000 );

    // 當計時器警報響起時遊戲中止
    while (!alarm) {
        // 玩家繼續遊戲
        yield table;
    }

    // 通知玩家遊戲已結束
    table.messages[2] = "CLOSED";

    // 裁判宣佈時間到了
    yield "Time's up!";
}
} );

  這裏咱們用table來模擬控制令牌以解決咱們上面說的那些特定領域的問題,這樣就能很好地來描述當一個玩家將球打回去的時候控制權被yield給另外一個玩家。*referee()中的while循環表示只要秒錶沒有停,程序就會一直yield table(將控制權轉移給另外一個玩家)。當計時器結束時退出while循環,referee將會接管控制權並宣佈"Time's up!"遊戲結束了。

  再來看看*player() generator的實現代碼(咱們使用兩個實例):

function *player(table) {
    var name = table.messages[0].shift();
    var ball = table.messages[1];

    while (table.messages[2] !== "CLOSED") {
        // 擊球
        ball.hits++;
        message( name, ball.hits );

        // 模擬將球打回給另外一個玩家中間的延遲
        yield ASQ.after( 500 );

        // 遊戲繼續?
        if (table.messages[2] !== "CLOSED") {
            // 球如今回到另外一個玩家那裏
            yield table;
        }
    }

    message( name, "Game over!" );
}

  第一個玩家將他的名字從message數組的第一個元素中移除("ping"),而後第二個玩家取他的名字("pong"),以便他們都能正確地識別本身(譯:注意這裏是兩個*player()的實例,在兩個不一樣的實例中,經過table.messages[0].shift()能夠獲取各自不一樣的玩家名字)。同時兩個玩家都保持對共享球的引用(使用hits計數器)。

  當玩家尚未聽到裁判說結束,就「擊球」並累加計數器(並輸出一個message來通知它),而後等待500毫秒(假設球以光速運行不佔用任什麼時候間)。若是遊戲還在繼續,他們就yield table到另外一個玩家那裏。就是這樣。

  在這裏能夠查看完整代碼,從而瞭解代碼的各部分是如何工做的。

 

狀態機:Generator協同程序

  最後一個例子:將一個狀態機定義爲由一個簡單的helper驅動的一組generator協同程序。Demo(注意:在支持ES6 JavaScript的最新版的FireFox nightly或Chrome中查看generators是如何工做的)。

  首先,咱們定義一個helper來控制有限的狀態處理程序。

function state(val,handler) {
    // 管理狀態的協同處理程序(包裝器)
    return function*(token) {
        // 狀態轉換處理程序
        function transition(to) {
            token.messages[0] = to;
        }

        // 默認初始狀態(若是尚未設置)
        if (token.messages.length < 1) {
            token.messages[0] = val;
        }

        // 繼續運行直到最終的狀態爲true
        while (token.messages[0] !== false) {
            // 判斷當前狀態是否和處理程序匹配
            if (token.messages[0] === val) {
                // 委託給狀態處理程序
                yield *handler( transition );
            }

            // 將控制權轉移給另外一個狀態處理程序
            if (token.messages[0] !== false) {
                yield token;
            }
        }
    };
}

  state(..) helper爲特定的狀態值建立了一個delegating-generator包裝器,這個包裝器會自動運行狀態機,並在每一個狀態切換時轉移控制權。

  依照慣例,我決定使用共享token.messages[0]的位置來保存咱們狀態機的當前狀態。這意味着你能夠經過從序列中前一步傳入的message來設定初始狀態。可是若是沒有傳入初始值的話,咱們會簡單地將第一個狀態做爲默認的初始值。一樣,依照慣例,最終的狀態會被假設爲false。這很容易修改以適合你本身的須要。

  狀態值能夠是任何你想要的值:numbersstrings等。只要該值能夠被===運算符嚴格測試經過,你就可使用它做爲你的狀態。

  在下面的示例中,我展現了一個狀態機,它能夠按照特定的順序在四個數值狀態間進行轉換:1->4->3->2。爲了演示,這裏使用了一個計數器,所以能夠實現屢次循環轉換。當咱們的generator狀態機到達最終狀態時(false),asynquence序列就會像你所指望的那樣移動到下一步。

// 計數器(僅用做演示)
var counter = 0;

ASQ( /* 可選:初始狀態值 */ )

// 運行狀態機,轉換順序:1 -> 4 -> 3 -> 2
.runner(

    // 狀態`1`處理程序
    state( 1, function*(transition){
        console.log( "in state 1" );
        yield ASQ.after( 1000 ); // 暫停1s
        yield transition( 4 ); // 跳到狀態`4`
    } ),

    // 狀態`2`處理程序
    state( 2, function*(transition){
        console.log( "in state 2" );
        yield ASQ.after( 1000 ); // 暫停1s

        // 僅用做演示,在狀態循環中保持運行
        if (++counter < 2) {
            yield transition( 1 ); // 跳轉到狀態`1`
        }
        // 所有完成!
        else {
            yield "That's all folks!";
            yield transition( false ); // 跳轉到最終狀態
        }
    } ),

    // 狀態`3`處理程序
    state( 3, function*(transition){
        console.log( "in state 3" );
        yield ASQ.after( 1000 ); // 暫停1s
        yield transition( 2 ); // 跳轉到狀態`2`
    } ),

    // 狀態`4`處理程序
    state( 4, function*(transition){
        console.log( "in state 4" );
        yield ASQ.after( 1000 ); // 暫停1s
        yield transition( 3 ); // 跳轉到狀態`3`
    } )

)

// 狀態機完成,移動到下一步
.val(function(msg){
    console.log( msg );
});

  應該很容易地跟蹤上面的代碼來查看到底發生了什麼。yield ASQ.after(1000)顯示了這些generators能夠根據須要作任何類型的基於promise/sequence的異步工做,就像咱們在前面所看到的同樣。yield transition(...)表示如何轉換到一個新的狀態。上面代碼中的state(..) helper完成了處理yield* delegation和狀態轉換的主要工做,而後整個程序的主要流程看起來十分簡單,表述也很清晰流暢。

 

總結

  CSP的關鍵是將兩個或更多的generator "processes"鏈接在一塊兒,給它們一個共享的通訊信道,以及一種能夠在彼此間傳輸控制的方法。

  JS中有不少的庫都或多或少地採用了至關正式的方法來與Go和Clojure/ClojureScript APIs或語義相匹配。這些庫的背後都有着很是棒的開發者,對於進一步探索CSP來講他們都是很是好的資源。

  asynquence試圖採用一種不太正式而又但願仍然可以保留主要結構的方法。若是沒有別的 ,asynquence的runner(..) 能夠做爲你實驗和學習CSP-like generators的入門。

  最好的部分是asynquence CSP與其它異步功能(promises,generators,流程控制等)在一塊兒工做。如此一來,你即可以掌控一切,使用任何你手頭上合適的工具來完成任務,而全部的這一切都只在一個小小的lib中。

  如今咱們已經在這四篇文章中詳細探討了generators,我但願你可以從中受益並得到靈感以探索如何革新本身的異步JS代碼!你將用generators來創造什麼呢?

 

原文地址:https://davidwalsh.name/es6-generators

相關文章
相關標籤/搜索