【正文】
講,已經(jīng)過去31年了,他老人家也已經(jīng)駕鶴西去。在這些方法看來,賦值語句與計算是等價的、同質(zhì)的、混合使用是天經(jīng)地義無需置疑的;IO只是局部領域內(nèi)的專有 問題(比如網(wǎng)絡通信)而不是全局性的問題。這些數(shù)學理論的推演告訴我們這樣一個結果: 在馮諾依曼型語言中IO問題不是局部的而是全局性的。當程序中引入越來越多的并行/并發(fā)背景 的時候,John Backus的遠見卓識就越顯示出他深邃的洞察力。比如兩臺以TCP連接的服務器,他們時鐘速度未必是等同的,即便是等 同的我們也無法忽略時鐘信號傳遞的延時。馮諾依曼機的程序指令是按照 CPU的時序順序執(zhí)行的,并發(fā)多任務程序也不例外。這使得自 然數(shù)集的若干個子集的并集同樣存在偏序性. 在馮諾依曼機上,任何與順序有關的問題,最為簡易的方式就是依靠偏序的傳遞性,從底層自低向上地一級一級傳遞CPU的時鐘序列。換而言之 Thread Model是一個放大了的馮諾依曼機,要在這個模型中處理并發(fā),我們必須面一個問題——時鐘校準。從物理學上講,信號傳遞是時鐘校準的唯一辦法??刂屏黩?qū)動模式?jīng)Q定了馮諾依曼機不允許外界的信號直接驅(qū)動指 令執(zhí)行。CPU在某個周期向其他時鐘源發(fā)送信號,在收到遠端時鐘的反饋信號后計算得出本地代碼序 列上的同步點,然后移動PC指針指向同步點。 基于信號查詢的校準方式,讓馮諾依曼機在處理并發(fā)問題上就像是帶著鐐銬跳舞。John Backus 告訴我們賦值語句其實是一種IO,它意味著賦值運算符兩邊數(shù)據(jù)的單向的數(shù)據(jù)流動。如 果以兩條賦值指令來驅(qū)動一次雙向數(shù)據(jù)交換,那么勢必就要同步兩條指令的先后執(zhí)行順序。于是這里我們陷入了類 似于相對論的邏輯困境。愛因斯坦為了避免這種邏輯無窮倒 退,引入了速度不變的光信號。set, exchange等等。隨著鎖機制的引入,鎖競爭,死鎖,粒度控制,各種并發(fā)性的麻煩滾滾而來。 控制流驅(qū)動還會導致馮諾依曼綜合癥中的 “Memory Wall”效應(另外兩個效應是”Energy Wall“和“Education Wall“)。隨著指令數(shù)量的增加Memory Wall會越來越厚,最終會阻塞控制流的運行,使其而無法及時響應IO信號。每次查詢完畢讓出CPU后就要執(zhí)行復雜的數(shù)據(jù)流動(比如 Register save/restore,Cache refill等)。在并發(fā)性達一定數(shù)量級后即便是Erlang這樣的語言也無法忽視“Memory Wall”的存在。 在馮諾依曼機統(tǒng)治計算機業(yè)的長達60多年,它帶來的Education Wall使得很多人特別是戰(zhàn)斗在開發(fā)第一線的程序員們很難想象有非馮諾依曼結構的存在。有一個傳說流傳甚廣——馮諾依曼制造了第一臺計算機ENIAC。馮諾依曼對計算機的興趣,來自于曼哈 頓工程中大量的數(shù)值計算。當時馮諾依曼發(fā)現(xiàn), 引用“the machines39。這一事實讓馮諾依曼更多的關注于順序指令代碼,企圖以此來保證并行操作決不會出現(xiàn)。相對于量子力學的可加性假設,反對Backus設計Fortran語言來說,馮諾依曼綜合癥還算不上是一 種失誤??梢韵胂?在那個電子管和打孔機的年代里,并行計算的難度恐怕是連這 位能心算無窮級數(shù)的天才級數(shù)學大師都望而生畏的東西。今日無數(shù)天才程序 員為之殫精竭慮的并行計算,其實是一個非常幽默的問題——如何在一臺原本設計用來杜絕并行計算的機器上進行并行編程? 由于馮諾依曼機在并行計算上的困難是本質(zhì)性的,很難在它之上做零碎的修改來徹底治愈馮諾依曼綜合癥。計算 機科學家們發(fā)現(xiàn),運算之間的時序性其實只取決于運算之間結果依賴性。假設我們有兩個CPU,顯然兩個加法可以 ,乘法的開始執(zhí)行時間依賴于最后一個加法的完成時間。在這種計算機上,首先需要依靠編譯器分析程序中的數(shù)據(jù)依賴性。編譯后的運算指令和Tag一起被合并到一種叫instruction storage的包。 CAM(Contentaddressable memory)與我們普通使用的線性編址隨機訪問的RAM(Random Access memory)不同。而CAM是給它一個data word,它會搜索地址空間給出所有存儲這個dataword的地址。 在DataFlow機上整個計算過程由數(shù)據(jù)流驅(qū)動控制流。程序的運算順序依賴于數(shù)據(jù)的輸入順序,而不依賴于系統(tǒng)的時鐘序列。這種模式在應對針對外部的并發(fā)信號時,程序無需進行毫無必要的輪詢操作,而是在信號到達的即刻立即響應處理數(shù)據(jù)。那個陰魂不散的賦值運算符所帶來的混亂也消失的無影無蹤。這 種現(xiàn)象并不奇怪,正所謂不識廬山真面目,只緣身在此山中。而在DataFlow模型下,Monad模型自然而然地回歸到了并發(fā)形態(tài)。 我們曾把Monad比作聯(lián)邦快遞,現(xiàn)在來看這個聯(lián)邦快遞所作的工作就是在快遞數(shù)據(jù)。IO Monad的各個部件在數(shù)據(jù)流驅(qū)動模型中一一對應必不可少的。Unix發(fā)明人Dennis M. Ritchie在The Evolution of the Unix Timesharing System中說道: 引用“Of course, the fundamental idea was by no means new。它只是一種特別形式的協(xié)程”Monad結構之所以會在PipeLine的層面上發(fā)揚光大,完全是因為pipeline所需要面對的是一群并發(fā)的進程。也就是說早期的Unix系統(tǒng)本身就是一個使用 Monad結構來構造的并發(fā)系統(tǒng)。隨著技術的發(fā)展,當操作系統(tǒng)層面上的工具無法應付越 來越復雜的并發(fā)問題時,我們才想起往傳統(tǒng)編程語言中增加并發(fā)性的支持。整個DataFlow的計算過程類似一棵表達式展開樹。在DataFlow誕生以來,計算機科學家們對DataFlow網(wǎng)結構特性進行了大量的研究。比如并發(fā)實體之間的狀態(tài)組合會隨并發(fā)數(shù)量的增加而導致狀態(tài)爆炸 (state space explosion problem).狀態(tài)爆炸意味著我們無法通過機械的手段來控制和檢驗并發(fā)狀態(tài)的變換和轉移,只能依靠人力來檢查。(A historical note on ``Geometry and Concurrency39。 現(xiàn)在大多數(shù)研究者相信, 順序型問題與并發(fā)并行是兩個完全不同的領域。這些前沿的研究方向,不是本文的重點。我們可以這樣猜測,Monad之所以與DataFlow模 型兼容,極有可能是因為它具有與并發(fā)系統(tǒng)相一致的幾何性質(zhì)。比如Exception就是一個并發(fā)性的問題。一個進程中的代碼出現(xiàn) 異常自己死亡,在死亡之前給link進程發(fā)送一個消息由link進程決定如何處理異常。在順序型領域種種軟件結構的代數(shù)性質(zhì)問題,在未 來都需要在并行/并發(fā)的幾何結構上進行重新探討。當然這些 問題是我們無法深究的還是需要留給數(shù)學家們?nèi)ヮ^疼,不過這些前沿理論給我們的編程實踐點亮前進的燈塔——“The world is parallel “。 DataFlow雖然在并行/并發(fā)中有著巨大的優(yōu)勢,但是至今沒有什么成功的商業(yè)型的應用。所以CAM現(xiàn)在只是用在一些特殊的設備上,比如網(wǎng)絡交換機和路由器基本依靠CAM來實現(xiàn)地址查找。如果切分的太細那么 硬件之間的通信網(wǎng)絡就無法承受大量的數(shù)據(jù)流。這些都是DataFlow模型在工程實踐方面尚不成熟的領域。在指令級層面,比如X86CPU處理器從Cache預取了一 批指令后,通過數(shù)據(jù)流分析(data flow analysis)分析找出沒有互相依賴的并發(fā)執(zhí)行的指令,送到幾個獨立的執(zhí)行單元進行亂序執(zhí)行(Outoforder execution),最后依靠Cache恢復指令序列。在高級語言層面,許多編譯器都能對源代碼做數(shù)據(jù)依賴分 析從而進行代碼重排優(yōu)化,將可并行的指令盡量靠近排列,以便CPU一次能夠盡量取到更多的并行指令。原始的同步/異步IO仍然是一種輪詢信號的做法,唯一的不同在于同步IO是Thread內(nèi)部輪詢,異步IO是程序員手工編寫輪詢編碼。它將多個臺機器上的獨立的運算過程,看成是一個DataFlow整體計算。只有當外部IO的信號到達的那一刻程序才會查詢事件注冊表找到對應的程序片斷開始執(zhí)行。但是它的缺點也是難以回避的。與Thread自底向上同步時鐘序列的模式不同。這也就意味著它必須以一種自頂向下地方式手工維護兩者時序的映射關 系。原本按照本地時鐘順序自然編寫的代碼,需要程序員手工切分成若干塊,以及手工維護復雜的狀態(tài)機來驅(qū)動代碼塊切換時所產(chǎn)生的數(shù) 據(jù)流動。第一種比較主流的方案,是以馮諾依曼機的角度出發(fā),以Thread設施兼容Event Driven,比如Fiber, coroutine, cooperativethread等等。缺點在于,第一無法消除馮諾依曼 機的Memory Wall, 是一個用空間復雜度換取時間復雜度的方案。在多核系統(tǒng) 上,多個Event Loop之間的任務調(diào)度是一個巨大的難題。 Lazy Evaluation將Event Driven的代碼片斷組合出Thread順序語義。2007年由Peng Li, Simon Marlow, Simon Peyton Jones 在Haskell上給出了第一個實現(xiàn)。因此傳統(tǒng)IO函數(shù)與賦值語句是可復合的,它的確讓我們的程序看上去自然舒適,而不像IO Monad那樣別扭。我們能夠放心的讓計算機運算1+1是因為有圖林機和lambda演作為保障。作為結構化語言的發(fā)明人John Backus在同一片演說里,反復強調(diào)了對賦值語句所帶來的困擾引用” Moreover, the assignment statement splits programming into two first world prises the right sides of assignment statements. This is anorderly world of