C++就像是巨型油輪,轉變航向非常耗時。但是隨著多核時代的到來,想繼續維持地位,C++不僅僅需要併發性(花了 6 年時間在C++11 標準里加入這一點),它更需程式設計正規化的大轉變。
為什麼要新的程式設計正規化?在物件導向的基礎上做點修改不行嗎?要說這個不得不談一談程式設計的本質:可組合性(composability)。作為人類,我們解決複雜問題的辦法是把它分解為更小的子問題。這是一個遞迴式的過程,問題不斷分解,直到子問題可以直接轉變為程式碼,最後再組合。可組合性的關鍵就在於每一層次都要隱藏複雜性。這也是物件導向程式設計成功的祕訣。物件內部複雜,但介面簡單。你通過介面來組裝部件,解決複雜問題。
但是對於併發性這個問題,物件隱藏了錯誤的細節。它們隱藏了共享(sharing)和變化(mutation)。資料競爭的定義是這樣:2 個或更多執行緒同時訪問同一段記憶體,至少 1 個寫入。換句話說,共享+變化=資料競爭。物件的介面不會告訴你物件內部可能發生了共享和變化。每個物件自身內部可能不存在資料競爭,但是它們的組合將不可避免的產生競爭。而如果不仔細分析每一次記憶體訪問,你是發現不了問題的。
Java 嘗試用互斥鎖(mutex)來緩解這個問題:每個物件都可以宣告 synchronized 方法來呼叫這個鎖。這不是一個可伸縮(scalable)的解決方案,它產生了不能忽略的效能開銷,所以程式設計師要對各種物件的內部細節瞭如指掌才能應用自如,而這恰恰是物件導向程式設計正規化竭力避免的。
更重要的是,鎖機制(locking)本身是不可以組合的。一個經典例子是銀行賬戶的存方法和取方法,這兩個方法通過一個鎖來同步。如果你試著把一個賬戶的錢轉到另外一個賬戶,問題出現了:如果不把鎖暴露出來,“錢已經從A帳號扣除,但還未存入B帳號”這個臨時狀態就無法避免。而如果鎖暴露在外,轉賬過程中兩把鎖被同一資源佔用,就可以能發生死鎖。[軟體事務性記憶體(Software Transactional Memory)對此提供了一個可組合(composable)的解決方案,但是除了 Haskell 和 Clojure,其他語言沒有實現能力。]
總之,如果我們想利用多核來提升效能,鎖機制絕對不是好辦法。我們要新方法。既然併發性的核心問題是共享和改變的衝突,解決辦法就是對它們進行控制。只要沒共享,我們就能對關鍵內容進行修改。比如修改區域性變數;或者通過深拷貝(deep copies)確定資源獨佔性,使用 Move 語義(move semantics),或者使用 unique_ptr。資源獨佔在訊息傳遞中扮演重要角色,可線上程間輕鬆傳遞大量資料。
多核程式設計的真正關鍵在於控制改變(mutation)。這就是函數語言程式設計在併發和並行領域穩步上升的祕密。一句話,函式式程式設計師找到了一種用看起來像是不可變資料(immutable data)來程式設計的方法。命令式程式設計師(imperative programmer)遇上不可變性(immutability),就如同烤肉師傅進了素食廚房,而 C++ 標準庫的幾乎所有資料結構都不適合這種程式設計方式,標準 vector 尤甚。一段連續的記憶體非常適合隨機或者連續存取,但如果記憶體有改變,你就不能把它共享給兩個執行緒。你可以用鎖來控制 vector,但是就如之前所講,用了鎖你就別想要效能和可組合性了。
函式式資料結構的優勢就在於它們表現得不可改變,所以多執行緒訪問無需同步。改變被構建(construction)取代:你建立一個新物件,是對原物件的複製,但進行了應有的改動。顯然,如果你想對 vector 進行如上操作,你會需要大量的拷貝。但是函式式資料結構本身就是為最大化共享而設計的,所以函式式的物件會與原始物件共享大部分資料。這種共享是透明的,因為原始物件是真正不可變的。
單連結串列就是這樣一個絕不簡單的典型資料結構。在鏈頭插入一個元素,只需要建立一個新節點,存入數值和原連結串列的指標即可(原連結串列不可改變)。還有很多易於克隆和修改的樹狀結構,比如紅黑樹、左偏樹。用函式式資料結構來實現並星演算法更容易,因為程式設計師完全不用考慮同步問題。
函式式資料結構,又稱為“永續性”(persistent)資料結構,天然具有可組合性。這是因為不可變的資料有可組合性,你可以用不變的小物件構建不變的大物件。而且用構建(construction)的方式來改變(mutate)也可以很好的組合。一個組合的永續性物件可以被克隆-改變,只記錄改變的部分,其他不變的部分可以安全的共享。
並行還帶來了不標準的控制流。大體上說,程式不再順序執行。程式設計師需要應對控制流的反向,從一個控制程式碼(handler)跳到另一個控制程式碼,對共享的已改變的狀態進行追蹤,等等。在函數語言程式設計中這不是什麼罕見的事。函式是一等公民,他們可以用多種方式組合。一個控制程式碼(handler)只不過是一種延續傳遞方式(continuation passing style)。延續(continuation)可以組合,雖然是以一種命令式程式設計師不熟悉的方式。函式式程式設計師有一個強大的組合型工具:單子(monad),與別的工具一道,它們可以使逆向的控制流線性化(linearize inverted flow of control)。一旦你弄懂了這個,你就會更加理解併發程式設計的庫的設計。
向函數語言程式設計正規化轉變是一件不可避免的事,而且越來越多的 C++ 程式設計師正在意識到這一點。以前我是一個在 C++ 討論會上聊 Haskell 和 monads 的怪人,現在情況變了。今年的 C++ 大會變化很大,最酷的人都在討論函數語言程式設計,“C++函式式資料結構”讓我贏得了最具啟迪獎。我認為這是 C++ 社群已經準備好進行改變的表現。
本文轉載自:伯樂線上