各語言併發問題

爲什麼需要併發?

曾有一段黃金時間,每18個月時鐘速度就會增加一倍。如果程序不夠快,那程序員只要等一等,計算機就會追上來了。

那個時代太美好,然而卻一去不復返了。CPU設計者們通過向計算機增加更多核心的方式試圖跟上摩爾定律。

這就造成了一個問題,這個問題被淹沒在營銷的辭藻中,而大多數程序員都沒領會它的含義。在新的世界中,我們的程序依然能夠每18個月提高一倍速度,但前提就是有效通過並行程序使用多個內核。

因此,作爲程序員,在並行環境下寫代碼的能力是個核心技能。這篇文章介紹了各種語言對並行和併發程序的支持情況。

經典併發原語

幾乎所有的操作系統都支持多線程執行,但併發程序員還需要解決另外兩個問題:

  • 共享的數據。如果併發訪問共享的數據,可能會產生無法預料的結果;
  • 線程間的信號傳遞。有時程序員需要控制線程的執行順序,一個例子就是程序員要讓線程在某個點等待另一個線程,讓它們按順序執行,不要超過另一個線程,或者某個關鍵區域最多隻能進入N個線程等。

編程語言提供了各種原語來輔助程序員控制以上的情況,下面是一些經典的原語:

  • 鎖(Lock,也叫作互斥鎖,Mutex):保證只有一個線程能進入指定的代碼區域;
  • 監視器:功能和鎖一樣,但比鎖好一些,因爲使用鎖時必須要解鎖;
  • (計數的)信號量(Semaphore):一種強大的抽象概念,能支持多種協同場景;
  • 等待並通知:功能相同,但比信號量弱一些,因爲程序員必須在等待之前處理丟失的通知觸發;
  • 條件變量:當某個條件觸發時讓線程睡眠或喚醒;
  • 帶有條件等待的通道和緩衝區:如果沒有線程接收信息的話,則監聽並收集信息(可以選擇有邊界的緩衝區);
  • 非阻塞數據結構(如非阻塞隊列、原子計數器等):這些智能數據結構支持從多個數據結構中訪問,而無需使用鎖,或者將鎖的使用控制在最少。

這些原語的功能有重疊。任何編程語言只需要幾個原語就能得到併發的全部力量。例如,鎖和信號量就能完成你能想到的任何併發場景。

原語的語言支持

選擇併發原語並不是依據它們的功能。不同的原語適用於不同的編程模型,它們對應問題的不同思考方式。不同的編程模型選擇了不同的原語集,以適合各自的編程模型。選擇哪一種取決於設計者的口味和語言的哲學。

我們來看看都有哪些選擇。

Java和C#

Java和C#的選擇就是沒有選擇,兩者都支持所有原語。

Java最初只支持監視器(synchronized關鍵字)和等待並通知,結果發現線程間的信號傳遞是個噩夢。我還記得我曾在“信號丟失”的問題上花了數個小時,最後依然無法得到正確的結果。

不久Java的設計者意識到這個錯誤,於是增加了concurrency包,支持所有原語,包括非阻塞數據結構。

唯一沒有原樣支持的原語就是通道和緩衝區。但是,如果你有需求,很容易用隊列和緩衝區進行模擬,當然你自己的實現絕對趕不上Go或Erlang的性能。

後起之秀C#從Java學了不少東西,它也支持所有原語。它還比Java多了幾個高階輔助結構,用於解決常見的問題,如barrier等。

更具體的內容請參見C# threading package:

https://msdn.microsoft.com/en-us/library/system.threading%28v=vs.110%29.aspx。

C和C++

C最初依靠系統調用實現多線程,結果就是犧牲了可移植性。於是,第三方併發庫出現了。不幸的是,由於語言並沒有規定API,因此許多庫都實現了不同的API。因爲C和C++是最接近操作系統的語言,最前沿的線程研究經常在這兩種語言上進行。

例如,簡單搜索下就能發現22個C++併發庫和6個C併發庫:

https://en.wikipedia.org/wiki/List_of_C%2B%2B_multi-threading_libraries;

https://stackoverflow.com/questions/5613646/threading-in-c-cross-platform。

它們不缺乏力量,所有這些庫都包含了廣泛的、最尖端的技術。但是,由於API多種多樣,因此熟知某個特定API的程序員寥寥無幾。

Erlang

Erlang天生就是爲併發設計的。Erlang以消息傳遞的方式爲程序員提供了對進程間交互的完全控制,程序員必須負責所有通信。這就是Erlang在多核計算機上能達到高性能的原因。

但是,這樣做是有代價的。Erlang不支持線程間的狀態共享,因爲共享的狀態會導致線程間同步,這種同步不由程序員直接控制,而且經常會導致性能降低。

其結果就是,用Erlang編程對於多數程序員來說就像外星語一樣。儘管它完全是函數式的,也無濟於事。

Erlang中的主要併發結構就是通道。它內置了緩衝區,而且支持在條件上等待。例如,可以要求通道等待,直到接收了滿足給定條件的消息。每個進程都有一個通道,並且只能從那個通道掃接收消息。

在實際應用中,由於Erlang被設計成函數式編程語言,因此基本上不需要共享內存鎖。但不幸的是,實際中經常存在這種情況。由於Erlang沒有共享內存,因此沒辦法鎖任何東西。但是,可以創建一個進程來代替鎖,像分佈式系統那樣,通過給該進程發送消息執行加鎖和解鎖的操作。

除非你是個熟知函數是編程的計算機語言專家,否則寫出的程序通常會極其複雜,並且難以調試。選擇Erlang就是用易用性換取了併發的支持。

如果希望瞭解更多,可以閱讀《Erlang for Concurrent Programming》和《The Hitchhiker's Guide to Concurrency》:

https://queue.acm.org/detail.cfm?id=1454463

http://learnyousomeerlang.com/the-hitchhikers-guide-to-concurrency

Go

Go很像Erlang,它主要的併發模型就是通道和緩衝區,並且支持條件等待(https://www.golang-book.com/books/intro/10)。其核心思想是:不要用共享內存進行通信,應該用通信來共享內存(https://golang.org/doc/effective_go.html#sharing)。

但是有個本質的區別,Go信任你會做正確的事情。Go允許你在線程間共享數據,同時還支持互斥鎖(https://gobyexample.com/mutexes)和信號量(https://github.com/golang/sync/blob/master/semaphore/semaphore.go)。此外,它還放寬了Erlang對於每個通道永久綁定到一個線程的限制,程序員可以隨意創建通道並隨意傳遞。

總之,Go希望程序員像使用Erlang那樣使用併發。但是,Erlang會做出強制,但Go相信你會寫對。如果Erlang代表了獨裁國家,Go就代表了自由州。

RUST

Rust也很像Erlang和Go。它使用通道進行通信,支持緩衝區,支持條件等待。與Go相似,它也放寬了Erlang的限制,允許使用共享內存(https://doc.rust-lang.org/book/second-edition/ch16-03-shared-state.html),支持原子級別的引用計數和鎖,並且允許將通道從一個線程傳遞到另一個線程。

但是Rust前進了一步。Go會完全信任程序員,而Rust會給程序員指派一名導師,如果程序員寫錯,導師就會提出警告。Rust的導師就是編譯器,它會做複雜的分析,確定線程間傳遞的值的所有者,並在可能出現問題時發出編譯錯誤。

下面的話引自RUST文檔。

所有者規則在消息傳遞中扮演了重要的角色,因爲它能幫我們編寫安全的併發代碼。使用Rust語言,就必須要付出考慮所有者的代價,但換來的好處就是能在併發編程中防止錯誤——通過值的所有者進行消息傳遞。

如果Erlang是獨裁國家,Go是自由州,那麼Rust就是保姆州。

調試併發程序是個噩夢,運氣差時需要花上好幾天的時間,所以Rust能在編譯器級別進行分析是非常有幫助的。

但是,如果你沒有併發編程經驗,卻想用Rust編寫併發程序,它就非常討厭了。不管你做什麼,它都會用艱澀的言語提醒你併發,改了之後它又會抱怨別的,周而復始。你不得不完全理解併發,在那之前用Rust編程可不太容易。

相反,Go模型把安全的責任交給了程序員,程序員通常會(錯誤地)認爲他的做法是正確的,但以後他就會付出代價。但是,只有當代碼進入生產環境,只有當用於遇到那種極端情況,並且錯誤被檢測到,檢測到的錯誤還被反饋給同一個程序員時,他纔會付出代價。這裏面的“只有”太多了。儘管這並不公平,但多數情況下那個程序員早就離職了。人類並不喜歡遲來的滿足感和長期規劃,所以程序員喜歡Go勝於Rust。

Rust想要幫忙,但幾乎沒人感謝它。沒人喜歡保姆。

更多的內容請閱讀《Rust和Go的併發原語比較》:

https://news.ycombinator.com/item?id=7851274

結論

談起併發的哲學時,不同的編程語言給你不同的選擇:自由州(Go),獨裁王國(Erlang),或保姆州(Rust)。

如果你希望瞭解更多內容,我可以推薦兩個資源。首先,閱讀《淺談信號量》(http://greenteapress.com/wp/semaphores/),它會教給你關於鎖和信號量的一切。其次,如果想理解通道和Erlang模型,可以看看MPI(http://mpitutorial.com/)。你可能認爲MPI是個死亡的語言,其實不是,今天許多科學模擬依然在使用MPI,天氣預報、汽車設計、新葯的發現都離不開它。科學就是由MPI推動的,MPI對併發的用法超出了我們的想像。

想嘗試一下的話,可以看看MPI通信原語:

http://www.mathcs.emory.edu/~cheung/Courses/355/Syllabus/92-MPI/group-comm.html

讀完上面兩個材料後,就可以理解併發的複雜性和可能性了,併發則需要一生的時間去精通。

原文:https://medium.com/@srinathperera/concurrency-ideologies-of-programming-languages-java-c-c-c-go-and-rust-bd4671d943f

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章