當前位置: 華文星空 > 知識

為什麽程式語言對異步編程都是很晚近才開始支持的?

2020-09-02知識

因為「直接在語言層面支持異步」並不是必需的;尤其是在經過大量實踐、真正把各種異步模型的優缺點徹底摸清之前,「語言層面的異步支持」反而是笨拙的、多余的。

異步模型本來就有很多很多種。從早期的中斷服務模型、多行程協同模型再到輕量級行程、執行緒乃至協程,業界也走過了很多彎路。

比如,早期的Windows 3.X搞的協作時多工就是協程思路管理的行程,在開發者良莠不齊甚至抱有敵意的環境下這麽搞完全是自尋煩惱;於是到了Windows 95就改用了「搶奪式多工」排程方案。

再比如,執行緒剛剛興起時,Linus堅持認為Linux的行程已經足夠用了,而且還有輕量級行程可以用;這致使Linux有很多年都不能支持執行緒(可以透過庫來支持,但庫做不到「一個行程內的多個執行緒同時利用多個CPU核心」:其實不怕麻煩的話,用行程來實作執行緒、用共享記憶體模擬「可共享的行程內資源」,也還是可以寫出「有多CPU支持的‘線’程庫」的,但那就有點行為藝術了)。

那時只有伺服器才較多使用了多CPU主機板——註意和現在的多核心不同,多CPU主機板上面有兩個以上的CPU插槽,允許插多顆物理CPU;而且多顆物理CPU各自有自己的記憶體,不同CPU之間要存取對方的記憶體就必須透過行程間通訊機制擠匯流排傳輸數據。

那麽,共享行程資源的執行緒顯然並不能從中得到任何好處——因此,Linus的決定顯然極有道理。

但是,CPU頻率提升遇到了瓶頸,多核多執行緒時代來臨。

「多執行緒CPU」指的是單個CPU核心內部制作了超量的邏輯單元,比如多個ALU、多個取指/譯碼電路,等等;這種CPU除非跑MMX/SSE/AVX之類指令,否則內部的大部份邏輯單元是不可能被充分利用的;但如果你有兩條執行緒,它們就可以較為充分的利用這一顆CPU核心內部的大量邏輯單元了。

「多核CPU」則是把多個CPU核心封裝在同一塊芯片內部,物理上看是一顆CPU,插在只有一個CPU插槽的主機板上、使用同一組記憶體條;但實際上,這顆CPU內部有若幹個物理的CPU核心,你完全可以給它們分別安排不同的任務……

不僅如此。

當年AMD最早把兩顆CPU核心做到同一片矽晶片上,首先推出了雙核CPU;Intel倉皇應對,把兩顆奔騰D晶片封裝進去,也推出了自己的雙核產品——後者被網友調侃為「膠水雙核」。雙方很是打了一番口水戰。

實際上,當年的競爭還要激烈得多,復雜得多。

比如,RISC和CISC之爭:CISC太復雜了,為了相容,甚至連8086的指令都能在Pentium上跑,這得浪費多少芯片面積、給編譯器造成多少麻煩……RISC決定拋掉相容性包袱,精心最佳化指令集,限制緩慢的記憶體存取指令的數量、把節省下來的芯片面積多造寄存器、透過寄存器視窗切換寄存器組,使得函式呼叫/執行緒切換不影響執行效率;同時由於它的指令集更簡潔,編譯器最佳化就更容易……

又有人說,我們幹嘛不在一顆CPU裏面造很多很多邏輯單元呢?然後在編譯器上下功夫,把大量使用者操作整合進一條指令——就好像「背包問題」一樣,一條指令整合盡可能多的操作、盡可能的充分利用CPU資源……這不就可以最大限度的提升指令執行效率了嗎?

這就是所謂的「超長指令字電腦」。

此外,還有超級純量CPU等很多奇思妙想。這些東西都失敗了;但這並不等於說它們都是錯的;相反,它們僅僅是「商業上未能取得成功」而已;它們的思路還是被現代CPU汲取、整合進來了。比如,指令多發射、SIMD以及RISC的很多先進經驗,現在都整合在x86裏面了。

顯然,CPU架構該如何發展,就連Intel/AMD都說不準。人類的能力,只能做到「走一步看一看」「提出一大堆看起來很美好的理論,到市場上比比優劣」,然後淘汰掉不合時宜的、保留其中更為優秀的。

甚至於,很多東西的走向僅僅決定於一個偶然——比如,如果不是AMD逼迫,或許intel就不會搞「膠水雙核」方案;那麽現在多核CPU的核心之間的聯系可能就會緊密得多,甚至直接走「超長指令字電腦」的路子都有可能。

但一旦「膠水雙核」的框架出來了、片內匯流排、共用cache以及有效性演算法等等發展起來了、適應這個架構的高級指令集設計出來了,CPU的發展路徑就被固定到現在這個模式上了。

類似的,顯卡搞通用計算、做GPGPU深入人心了,那麽CPU上面再搞AVX就要受限了——小打小鬧你可以靠低延遲獲勝;但真玩大的……怎麽可能玩的過GPU?

但另一方面,CPU做核芯顯卡,直接整合個GPU進去、然後再把GPU計算單元和CPU內部單元無縫融合……就好像80386CPU和80387數學協處理器被整合進486CPU一樣,這會不會是未來的發展方向呢?

回到問題:在多核多執行緒架構完全確立下來之前,你想讓程式語言如何支持異步編程呢?

在相關領域的研究/實踐足夠多、方案足夠成熟之前,你連「異步究竟應該做成‘協作式多工’還是‘搶占式多工’」都不可能知道——async/await並不是表面看來那樣,僅僅是一個簡單的關鍵字;它的背後必需存在一個合理的體系,一個異步執行框架,不然就沒法實作功能。

顯然,過早的和一個不成熟的方案繫結(比如你的語言特性完全繫結於Windows3.1的協作式模型),只會讓你的心血跟著這個不成熟的方案一起付諸東流。

因此,在時代來臨之前,在程式語言中添加「異步編程的支持」,顯然是有百害而無一利的。

這種東西就應該用庫支持。將來架構發展方向變了,也就是廢掉一個庫的問題,不會拖死一整個語言,對吧。

但到了現在,由於硬體架構發展方向越發清晰、固定,我們終於可以確定「異步編程模型」應該是什麽樣子了:

1、最上層是行程;行程是持有資源的最小單位

2、中層是執行緒;執行緒不持有資源,是CPU排程的最小單位

3、下層是協程;協程既不持有資源、也不必在意CPU排程,它僅僅關註「協作式的、自然的執行流程切換」

當然,細節肯定還是會千變萬化的;但大致來說這個整體圖景不太可能有大的變動了。

底層穩定下來了,語言的直接支持才可能跟上。

不然的話,你見過哪門程式語言換個新一代的CPU就得禁用若幹個關鍵字、或者把某些關鍵字的含義改變一番的?

python 2 to 3不過是風格上的少許改變,至今都還雞飛狗跳的。它要告訴你「因為windows 95上行程的含義和Windows 3.1有所不同,因此你必須檢查你的程式,在如下(省略五千字)情況下,請不要使用async關鍵字」或者「由於zen4改用了超長指令字架構,await在如下(省略一萬字)情況下無法正常工作」,你還不得去刨Guido的祖墳啊。

反過來說也對:一旦大部份語言和OS和CPU的某個特性繫結,那麽OS和CPU就沒法改變設計了。比如,如果現在有人提出了一個比執行緒/行程模型更優越的新架構,OS/CPU制造商就不得不在「拋棄一部份語言和它們的使用者」和「使用新架構但給出一個效能有所損失的相容層」和「挺好的,但……算了,雪藏起來吧」之間做出選擇。

再換句話說:程式設計語言本就應該和OS和CPU的具體實作脫耦;在行程-執行緒-協程模型確立之前,提供async/await就產生了「與OS或CPU的緊密耦合」,無論對編譯器商還是對OS開發者還是CPU制造者,這都是個極大的不利。

註意這是「條件不成熟」,並不是什麽「滯後」。

舉例來說,AIO至今沒有一個統一的方案(Windows仍然堅持自己的‘完成埠’,Linux則繼續它的epoll,而BSD覺得kqueue挺好的);因此沒有任何語言提供AIO的直接語法支持——你完全可以用諸如libevent之類別庫寫出跨平台的高效能網路服務程式;這樣將來倘若有人搞出來一個真正天才的、足以一統天下的方案,你只需提供一個介面相容的轉接層,原本的程式就仍然能執行。

但讓某種語言直接提供支持?將來必定是天下大亂:改變語意,原有的計畫統統死掉;內部做判斷、給新計畫用新語意,無論編譯器還是程式編寫的工作都會變得極其復雜、易錯。

想想新的入門者必須區分3.7.13之前和之後的版本,不同版本await關鍵字含義大體相同但又有微妙的差別……

這也太瘋狂太不負責任了,對吧。