Promise不是Callback

這一篇是在實際工程中遇到的一個可貴的例子;反映在Node裏兩種編程範式的設計衝突。這種衝突具備普適性,但本文僅分析問題本質,不探討更高層次的抽象。node


我在寫一個相似HTTP的資源協議,叫RP,Resource Protocol,和HTTP不一樣的地方,RP是構建在一箇中立的傳輸層上的;這個傳輸層裏最小的數據單元,message,是一個JSON對象。npm

協議內置支持multiplexing,即一個傳輸層鏈接能夠同時維護多個RP請求應答過程。編程

考慮客戶端request類設計,相似Node內置的HTTP Client,或流行的npm包,如requestsuperagentpromise

能夠採用EventEmitter方式emit errorresponses事件,也能夠採用Node Callback的形式,須要使用者提供接口形式爲(err, res) => {}的callback函數。服務器

隨着async/await的流行,request類也能夠提供一個.then接口,用以下方式實現(實際上superagent就是這麼實現的):併發

class Request extends Duplex {
    constructor () {
        super()
        ...
        this.promise = new Promise((resolve, reject) => {
            this.resolve = resolve
            this.reject = reject
        })
    }
    
    then (...args) {
        return this.promise.then(...args)
    }
}

RP的實際設計,形式和你們熟悉的HTTP Client有一點小區別,response對象自己不是stream,而是把stream作爲一個property提供。換句話說,callback函數形式爲:異步

(err, { data, chunk, stream }) => {}

若是請求返回的不是stream,則data或者chunk有值;若是返回的是stream,則僅stream有值,且爲stream.Readable類型。async

這個形式上的區別和本文要討論的問題無關。函數


RP底層從傳輸層取二進制數據,解析出message,而後emit給上層;它採用了一個簡單方式,循環解析收到的data chunk,直到沒有完整的message爲止。性能

這意味着能夠在一個tick裏分發多個消息。request對象也必須可以在一個tick裏處理多個來自服務端的消息。

咱們具體要討論的狀況是服務器連續發了這樣兩條消息:

  1. status 200 with stream
  2. abort

第一條意思是後面還有message stream,第二條abort指server發生意外沒法繼續發送了。

request對象收到第一條消息時,它建立response對象,包含stream對象:

this.res = { stream: new stream.Readabe({...}) }
// this.emit('response', this.res)
// this.callback(null, this.res)
this.resolve(this.res)

象註釋中emit或trigger使用者提供的callback,都沒有問題;但若是調用resolve,注意,Promise是保證異步的,這意味着使用者經過then提供的onFulfilled,不會在當前tick被調用。

接下來第二條消息,abort,在同一個tick被處理;但這個時候,由於使用者還沒來得及掛載上任何listener,包括error handler,若是設計上要求這個stream emit error——很合理的設計要求——此時,按照Node的約定,error沒有handler,整個程序crash了。


這個問題的dirty fix有不少種辦法。

首先request.handleMessage方法,若是沒法同步完成對message的處理,而message的處理順序又須要保證,它應該buffer message,這是node裏最多見的一種synchronize方式,表明性的實現就是stream.Writable

但這裏有一個困難,this.resolve這個函數沒有callback提供,必須預先知道運行環境的Promise實現方式;在node裏是nextTick,因此在this.resolve以後nextTick一下,同時buffer其它後續消息的處理,可讓使用者在onFulfilled函數中給stream掛載上handler。


這裏能夠看出,callback和emitter其實是同步的。

當調用callback或者listener時,request和使用者作了一個約定,你必須在這個函數內作什麼(在對象上掛載全部的listener),而後我繼續作什麼(處理下一個消息,emit data或者error);這至關因而interface protocol對順序的約定。

咱們能夠稱之爲synchronous sequential composition,是程序語義意義上的。

對應的asynchronous版本呢?

若是咱們不去假設運行環境的Promise的實現呢?它應該和同步版本的語義同樣對吧。


再回頭看看問題,假如stream emit error不會致使系統crash,使用者在onFulfilled拿到{ stream }這個對象時,它看到了什麼?一個已經發生錯誤後結束了的stream。

這個可能使用上會難過一點,須要判斷一下,但還感受不出是多大的問題。

再進一步,若是是另外一種狀況呢?Server在一個chunk裏發來了3個消息;

  1. status 200 with stream
  2. data
  3. abort

這個時候使用者看到的仍是一個errored stream,data去哪裏了呢?你還能說asynchronous sequential composition的語義和synchronous的一致麼?不能了對吧,同步的版本處理了data,極可能對結果產生影響。

在理想的狀況下,sequential composition,不管是synchronous的,仍是asynchronous的,語義(執行結果)應該一致。

那麼來看看如何作到一個與Promise A+的實現無關的作法,保證異步和同步行爲一致。

若是你願意用『通信』理解計算,這個問題的答案很容易思考出來:假想這個異步的handler位於半人馬座阿爾法星上,那咱們惟一能作的事情是老老實實按照事件發生的順序,發送給它,不能打亂順序,就像咱們收到他們時同樣。

可是當咱們把進來的message,翻譯實現成stream時,沒能保證這個order,包括:

  1. abort消息搶先/亂序
  2. data消息丟失了

這是問題的root cause,當咱們異步處理一個消息序列時,前面寫的實現break了順序和內容的完整性。


在數學思惟上,咱們說Promise增長了一個callback/EventEmitter不具有的屬性,deferred evaluation,是一個編程中罕見的temporal屬性;固然這不奇怪,由於這就是Promise的目的。

同時Promise -> Value還有一個屬性是它能夠被不一樣的使用者訪問屢次,保持了Value的屬性。

這也不奇怪。

只是Stream做爲一種體積上能夠爲無窮大的值,在實踐中不可能去cache全部的值,把它總體當成一個值處理,因此這個能夠被無限提取的『值』屬性就消失了。


可是這不意味着stream做爲一個對象,它的行爲,不能延遲等到它被構造且使用後纔開始處理消息。

一種方式是寫一個stream有這種能力的;stream.Readable有一個flow屬性,必須經過readable.resume開始,這是一個觸發方式;另外一個方式是有點tricky,能夠截獲response.stream的getter,在它第一次被訪問時觸發異步處理buffered message。

這樣的作法是不須要依賴Promise A+的實現的;但不是百分百asynchronous sequential composition,由於stream的handler確定是synchronous的。

徹底的asynchronous能夠參照Dart的使用await消費stream的方式。

它的邏輯能夠這樣理解:把全部Event,不管哪裏來的,包括error,都寫到一個流裏去,用await消費這個流;但實際上在await返回的時候仍然面對一個狀態機,好處是

  1. throw給力;
  2. 流程等待方便,即處理流輸出的對象時還能夠有await語句,在取下一個流輸出的對象以前,至關於一種blocking;但這種blocking須要慎重,它是反併發的;

總結:

Node的Callback和EventEmitter在組合時handler/listener是同步的;Promise則反過來保證每一個handler/listener都是異步組合,這是二者的根本區別。

在順序組合函數(或者進程代數意義上的進程)上,同步組合是緊耦合的;它體如今一旦功能上出現什麼緣由,須要把一個同步邏輯修改爲異步時,都要大動干戈,好比原本是讀取內存,後來變成了讀取文件。

若是程序天生寫成異步組合,相似變化就不會對實現邏輯產生很大影響;可是細粒度的異步組合有巨大的性能損失,這和現代處理器和編譯器的設計與實現有關。

真正理想的狀況應該是開發者只表達「順序」,並不表達它是同步仍是異步實現;就像前面看到的,實際上同步的實現都有能夠對應的異步實現,差異只是執行效率和內存使用(buffer有更多的內存開銷,同步處理實際上更可能是『閱後即焚』);

但咱們使用的imperative langugage不是如此,它在強制你表達順序;而另一類號稱將來其實狗屎的語言,在反過來強制你不得表達順序。

都是神經病。學術界就不會真正理解產業界的實際問題。

相關文章
相關標籤/搜索