這篇文章的標題是一個π表達式,結尾是一段JavaScript代碼,和這個表達式的含義徹底一致,或者說,完成了這個表達式的估值。express
π演算(π calculus)是一種表達併發過程(process)的數學語言,和λ在形式上有不少相似之處;λ已是公認的計算模型,它和圖靈機或遞歸理論(Recursion Theory)描述的計算模型是等價的,可是這個計算模型沒法描述併發計算。編程
π沒有λ那樣的地位;它只是描述併發計算的模型之一,在計算數學領域科學家們尚未對併發計算模型達成一致,每一種併發計算模型都強調了併發計算的某些方面的特性但尚未一個模型成爲λ那樣的經典模型能夠解釋其餘全部模型,或者證實其等價性;數學家們選擇使用不一樣的模型研究他們感興趣的併發計算特性。promise
π是併發計算模型中最精簡的一個,具備最小的概念元素集;它只包含兩個概念:過程(process)和通信(channel)。過程是一個計算單元,計算是經過通信來完成的。併發
「計算是經過通信完成的」對於不熟悉數學理論的人來講不容易理解;這裏也沒有更好的解釋;推薦看一點關於λ的介紹,感覺一下計算是如何經過變量名binding和替換(substitution)完成的。在數學上,λ能夠被encode在π裏,可是不應對encode一詞產生基於其天然語言含義的聯想,若是想了解它的確切含義,請系統學習λ和π的相關理論。dom
題目是一個π表達式,也能夠看做是用π演算寫下的一段程序,即把π看成一種編程語言,雖然這個表達式沒有什麼實際用處,也沒有人會打算用π做爲編程語言寫下實際的程序;可是在對照了題目的π表達式和文中的JavaScript代碼以後,我相信你會得到一些新感覺,關於對併發的理解,什麼是描述併發的最小概念集。async
這篇文章是一個起點,若是我有時間,還會逐步解釋在JavaScript裏的callback,emitter,promise,async/await,等等,都不是在併發計算模型意義上的最小概念,它們更多的考慮了實際使用中最經常使用需求、最簡潔書寫、最容易學習等工程特性或需求,但同時也會遇到一些棘手的問題;而這篇文章,就是在探索對併發編程而言,最原始(primitive
)的東西是什麼。編程語言
和λ同樣簡單的是,在π裏只有name,name表示一個channel。ide
標題裏的π表達式(π-term)沒有包含全部的π符號,只包含了其中的一部分;解釋以下:函數
'.'
(dot)能夠被理解爲繼續(continuation),或者反過來理解,阻塞(blocking);它的意思是前綴必須先完成通信,即接收或發送完成,'.'
以後的表達式才能夠開始執行,或者稱爲估值;這是π裏惟一表達順序(order)的地方;你能夠給一個表達式取一個名字,習慣上使用大寫字母P, Q, R...學習
例如:
若是 P = z<a>.0,最左側的表達式就能夠寫成x(z).P;
若是 P = x(z).z<a>.0,則最左側的表達式就能夠寫成P;
若是
- P = x(z).z<a>.0
- Q = x<w>.y<w>.0
- R = y(v).v(u).0
則整個標題的表達式能夠寫成 P | Q | R
有時候咱們採用π.P的寫法表示一個通用的π表達式,而不關心這個表達式裏的π具體是那種前綴;
固然也能夠定義: U = P | Q | R,它仍然是π表達式。
每一個π表達式都表達了一個過程。
'|'
(vertical pipe)在π裏的含義是併發組合,能夠看做過程的運算符;U = P | Q | R就能夠理解爲過程U是由三個過程併發組成的。
π裏的另外一個組合過程的運算符是
'+'
,summation,咱們暫不介紹。
標題的表達式裏還有一個符號0
,0
表示一個無行爲的過程(inaction)。
這一段能夠在看了後面的代碼以後再回來對照理解。
Free name的含義和λ或編程語言裏的定義一致;它是bound name的反義詞;bind的意思和λ也是一致的(λx);
在π裏有兩個符號會bind name,標題裏的表達式只出現了一個,即輸入前綴,例如:x(z)。這很容易理解,在JavaScript代碼裏咱們經常使用listener函數接收消息:
emitter.on('data', data => { // do something with data })
這裏的data
變量的scope就是在這個匿名函數內的,即bound name。一個過程P的Free name是它和外部產生行爲交互的惟一方式。
這裏是π process教材裏的描述:
The free names of a process circumscribe its capabilities for action: for a name x, in order for P to send x, to send via x, or to receive via x, it must be that x ∈ fn(P). Thus in order for two processes to interact via a name, that name must occur free in both of them, in one case expressing a capability to send, and in the other a capability to receive.
from π calculus by Davide Sangiorgi and David Walker
譯:
一個過程的free name決定了它的行爲能力,對於過程P中的name x,若是P可以:
- 發送x
- 經過x發送其餘name
- 經過x接收其餘name
x必須是P的free name。因此若是兩個過程須要經過一個name交互,這個name必須在兩個過程當中都是free name,其中一方用於發送,另外一方用於接收。
這個詞在編程界被用爛了。可是它的含義沒有什麼高大上的地方。一個數學公式的形式變換就是reduction,固然咱們正常狀況下是但願它越變越簡潔的(因此叫reduce),除了你的陰險的數學老師會在出題時有個相反的邪惡目的。
π只有一個reduction:
x<y>.P | x(z).Q -> P | Q[y/z]
含義是y從channel x發送出去以後,P才能夠繼續執行;同時x(z)前綴收到了y,Q得以繼續執行,此時Q裏的全部z都要替換成y。
在編程中:
x(z).Q意味着若是x還沒有收到數據,Q不能開始執行;這個input prefix在程序語言裏很容易實現,就是常見的listener或者callback。
x<y>.P意味着只有y被讀走,P纔會開始執行;這相似lazy實現的stream,在數據沒有被讀走以前,不會再向buffer填充數據;在編程語言裏實現這個表達式,和實現readable stream時,收到drain
事件纔開始填充數據相似。
咱們先假定存在一個構造函數或工廠方法,能夠構造一個channel對象;咱們先不回答channel如何構造,以及它內部是什麼。
咱們要求channel對象有一對接口方法,按照π的邏輯應該叫作send和receive;
注意在π裏咱們沒有類型和值的概念,一切變量皆channel,寫成代碼就是一切變量皆channel對象,經過channel傳遞的變量也是channel,這是π系統的重要特性之一:pass channel via channel(由於它會讓name突破一個scope使用)。
咱們首先發現這個表達式裏的free name都得先聲明(why?);x,y,w,a都聲明成channel(a在這個例子中沒有實際用於通信,能夠是任何東西)。
第一段代碼就是這個樣子。
class Channel { // placeholder } const channel = () => new Channel() const x = channel() const y = channel() const w = channel() const a = channel()
'.'
(dot)所表達的繼續,咱們能夠用調用一個函數來實現;.0
,能夠用調用空函數(() => {}
)表示;
第一個表達式:x(z).z<a>.0,能夠這樣寫:
x.receive(z => z.send(a, () => {}))
receive方法形式上是提供一個函數f
做爲參數,channel x在接收到值z的時候調用這個函數f(z)
;
第二個表達式:x<w>.y<w>.0,能夠這樣寫:
x.send(w, () => y.send(w, () => {}))
注意這裏要send成功以後才能繼續而不是調用send後就繼續,因此不能寫成:
x.send(w) y.send(w)
最後一個表達式:y(v).v(u).0
y.receive(v => v.receive(u => (() => {})()))
到這裏咱們寫完了使用者代碼;在使用的時候咱們也給Channel類的接口下了定義;若是你問哪一個表示併發的vertical pipe(|
)哪裏去了?你想一下,我在文章的最後給出問題的答案。
在實現Channel
類以前咱們還要考慮一個順序問題。
π裏的reduction是兩個併發過程之間發生的;在reduction的時候咱們要調用兩個函數實現'.'
表示的繼續,分別是發送者繼續和接收者繼續,咱們是否應該約定一個固定的順序?
答案是不該該;對於這裏寫下的玩具代碼咱們甚至故意加入了隨機性,這纔是併發的含義,併發過程之間木有固定執行順序。
咱們先定義一個reduce函數;它的前提是send和receive兩個方法都被調用過了;這裏存在兩種順序可能性:若是receive先被調用了,f被保存下來直到send被調用,這和常見的listener沒有區別;但π也容許反過來的順序,send先被調用了,則c和f都被保存下來,等到receive調用的時候再使用,這就是π裏的兩個前綴會block後面的表達式估值的實現。
不管send和receive的實際調用順序如何,咱們都但願reduce能夠隨機執行sender和receiver提供的回調函數。
class Channel { reduce () { if (!this.sendF || !this.receiveF) return let rnd = Match.random() if (rnd >= 0.5) { this.sendF() this.receiveF(this.c) } else { this.receiveF(this.c) this.sendF() } } send (c, f) { this.c = c this.sendF = f this.reduce() } receive (f) { this.receiveF = f this.reduce() } }
寫出reduce以後send和receive就是無腦代碼了;在標題的表達式裏每一個channel都只用了一次,因此咱們不用在這裏糾結若是重複發送和接受的狀況如何解決;各類參數檢查和錯誤處理也先無論了,先跑起來試試。
最後全部的代碼都在這裏,加了一點打印信息,你能夠運行起來感覺一下,也思考一下:
class Channel { constructor (name) { this.name = name } reduce () { if (!this.sendF || !this.receiveF) return console.log(`passing name ${this.c.name} via channel ${this.name}`) let rnd = Math.random() if (rnd >= 0.5) { this.sendF() this.receiveF(this.c) } else { this.receiveF(this.c) this.sendF() } } send (c, f) { console.log(`${this.name} sending ${c.name}`) this.c = c this.sendF = f this.reduce() } receive (f) { console.log(`${this.name} receiving`) this.receiveF = f this.reduce() } } const channel = name => new Channel(name) const x = channel('x') const y = channel('y') const w = channel('w') const a = channel('a') x.receive(z => z.send(a, () => console.log('term 1 over'))) x.send(w, () => y.send(w, () => console.log('term 2 over'))) y.receive(v => v.receive(u => (() => console.log(`term 3 over, received ${u.name} finally`))()))
爲何vertical pipe表示的併發組合沒了呢?由於連續執行上面代碼段裏最後三句的時候,就是併發了;必定要說語言上什麼符號對應了'|'
的話,對於JavaScript就是;
號了;它原本在語言上是statement的順序組合,在咱們這個代碼裏,就是併發組合了。