Lock Free?還是多入口?

2021-06-19 11:29:03 字數 3179 閱讀 1117

最近一段時間,感覺大家對於lock-free的興趣又高漲了起來,lock-free大有包治百病、一統江湖之勢,特寫下此文,希望對圍觀者有所幫助。

讓我們先從乙個簡單的場景開始:考慮乙個需要頻繁併發訪問的freelist,這應該是許多應用程式中最常見的結構了,如果我們使用基本設計,用乙個簡單的mutex量來保護這個freelist,那麼在高併發環境下它很容易成為效能的瓶頸。然後該如何優化呢?一種是多入口,將freelist拆分成多個,每個用不同的mutex來保護;另一種是不使用mutex,直接使用lock-free。本文不想過多地討論實現細節,關於lock-free fifo的設計,網上已經有太多的資源了(但令我驚奇的是,直到今天,linux並沒有提供封裝好的api);至於多入口,有興趣的同學可以去看一下oracle的log buffer裡關於多copy latch的設計,這是乙個經典的實現。

我們的問題是:那種設計更好?多入口?還是lock-free?答案是:多入口完勝lock-free。原因很簡單:lock-free只是將資料操作原子化,避免了mutex設計中的wait/sleep之類的設計,但它並沒有改變單點衝突的本質,從延展性的角度上看,lock-free相對於mutex,只能算是「五十步笑百步」;而多入口,則是真正地將單點衝突變為多點衝突。

將這兩者放在一起比較是不公平的,因為它們並不是乙個層面的東西,事實上,你可以在把freelist改造為多入口之後,再將每個入口改造為lock-free。引入這個問題的目的,是希望引起關於併發優化的問題全景的思考。所以回到乙個更一般性的問題:併發優化需要解決哪幾個層面的問題?它們各自的重要性是怎麼樣的?以下是我個人的理解,按照重要性排列,僅供參考:

1)減少呼叫次數

沒有任何一種臨界區優化手段的效果能夠和減少呼叫次數相比,如果你能夠減少呼叫次數,那麼意味著優化工作的意義已經不限於目標臨界區本身。這種優化更多地來自於協議本身,這也是乙個優秀的併發系統最有價值的部分。比如,如果你採用mvcc,那麼你可以消除資料讀寫之間的衝突,同時也意味著你不需要維護行鎖,而後者在傳統資料庫的實現中是最主要的幾個併發瓶頸之一。再比如索引的設計,你的索引支援併發的smo操作嗎?支援乙個執行緒在做smo的同時,另乙個執行緒可以繼續訪問這些資料嗎?優秀的索引併發協議設計消除了譬如全域性鎖之類的顯而易見的併發瓶頸,也決定了你的系統的寫擴充套件能力。

2)多入口改造

無論何時,無論何地,當你需要優化乙個臨界區的時候,你都應該首先考慮如何多入口化,如果你的臨界區能夠被改造成多入口,那麼它能夠帶來的延展性提公升效果是後面將要提到的這些技術無法到達的。很多時候,多入口意味著你需要提供更加精細的訪問粒度,在資料庫的設計中,所有重要的資料結構,比如sga、字典快取、鎖表、...等,無一例外,都是多入口的。

更細的訪問粒度看上去象是一種靜態的資源切分。而在前面的例子中,freelist的多入口改造看上去像是一種動態的資源切分,因為你並不在乎獲得哪個資源,此時多入口訪問的重點在於平均應用對這些入口的訪問,並採用非繫結的模式(比如使用nowait方式逐一嘗試各入口),基本上都是一些非常簡單、但是很實用的實現級優化細節。

3)優化臨界區本身

哪怕只是減少一次記憶體訪問,對於減少臨界區的長度都是有意義的,**級別的優化的重要性無須贅述。更多時候,優化臨界區需要進行臨界區拆分,臨界區拆分和多入口改造有些類似,差別在於前者是面向資料的,而後者是面向**路徑的。臨界區拆分需要考慮幾個問題:一、要保證拆分後的正確性,這是一切有用功的基礎;二、拆分後的臨界區通常需要引入更多的同步量(比如mutex),所以需要仔細考慮如何避免引入死鎖;三、同步量的實現是有代價的,這種代價隨著機器效能的進步會相對地越來越大,所以,過於精細的臨界區拆分可能適得其反。

私有化改造是一種比較高階的臨界區拆分技術,它的本質是區分對臨界區訪問的不同路徑,將其中90%的訪問路徑定義為讀者(如果它們是彼此相容的),將另外10%的訪問路徑定義為寫者(如果它們和其它所有人都衝突);私有化改造將讀者的訪問私有化(使用本地的同步量),而將衝突交由寫者來處理,寫者訪問全域性的同步量,並逐一同步讀者的本地同步量來解決衝突。簡單地說,就是「犧牲寫者,讓讀者的處理效率最快化」。了解rcu嗎?這是一種經典的私有化改造的設計。

私有化改造在資料庫中可以用來解決一些非常頑固的衝突,這些衝突所涉及的資源無法或難以再被細分。還記得私有日誌快取嗎?(參看我前面的微博「關於私有日誌快取」),它是一種典型的私有化改造,讀者是誰?是每個事務各自產生日誌;寫者是誰?是當發生頁面更新衝突時,由後來者負責將讀者的私有日誌同步。通過私有日誌快取,我們極大地降低了公有日誌快取上的併發衝突。在神通資料庫中,私有化改造還被用於其它的併發結構,以後有機會的話,我可以為大家介紹一下,如何將資料庫的鎖表(用於實現字典鎖、表級鎖)私有化。

4)優化同步機制本身

最後,是優化同步機制本身,我們把它稱為「最後一公里」的優化,它的效果仍然是不可忽視的。這一工作通常可以分為兩個層面:首先,是為每個臨界區選擇合適的同步機制,是選擇互斥鎖,還是讀寫鎖?是選擇基於spin+sleep的mutex,還是選擇基於spin+wait的latch?是選擇鎖,還是直接使用lock-free?

其次,是對每個臨界區的同步機制進行tuning,比如臨界區a上的mutex的sleep時間多少毫秒比較合適?臨界區b上的latch的spin次數多少更優?這看上去更像是面向特定應用場景的優化。所以對於這兩個層面來說,前者適合於產品設計階段,後者則更多發生於系統運維階段。

以上就是我理解的併發優化你需要關注的四個層面的問題。然後回到lock-free本身,本文前面所舉的例子反映了早期對lock-free的研究成果,它們基於一些特定的資料結構(比如fifo),來實現資料訪問邏輯的原子化。在實際實現中,通過靈活地應用memory-barrier,lock-free可以應用於更多的場景。比如如果對於乙個物件的訪問是讀多寫少的,我們可以採取一種簡單的邏輯:為這個物件增加乙個時間戳;更新物件的前後對時間戳原子加1;讀取物件的前後獲取時間戳的當前值,如果這兩個值相等,那麼表示讀取操作是原子性的,否則就重試。

這看上去更像是名副其實的lock-free,我們沒有使用昂貴的cas操作,我們所要做的只是在**中加入一些memory-barrier,以保證上述邏輯會以正確的順序執行。問題在於:讀者可能在寫者更新物件的同時進行訪問,所以你需要保證讀者不會在訪問的過程中崩潰,這意味著你的臨界區通常只能訪問固定位址的資料,而對於一些稍微複雜的結構(比如指標、資料庫中的行目錄間址),你可能需要引入足夠強勁的容錯邏輯來保證程式的魯棒性,很多時候,你為此所付出的代價並不能被使用lock-free而得到的好處所覆蓋。

最後是乙個簡單的關於lock-free的總結:客觀地說,lock-free是一種非常有效的併發優化手段,儘管它們並不能支援更加複雜的臨界區;大多數時候,lock-free被用於所謂的「最後一公里」的效能優化,它們不應,也不能用於為應用糟糕的併發設計而免單。

lock free 單寫多讀的迴圈記憶體

lcok free 的單寫多讀迴圈記憶體從無鎖的單寫單讀迴圈記憶體變化而來。所以這個問題分2節來介紹 單寫單獨 迴圈記憶體 lock free 單寫多讀迴圈記憶體 1.常規的單寫單讀迴圈記憶體通過設定讀指標和寫指標保證了執行緒的安全。其實現如下 寫資料到記憶體裡 m uwritepos 宣告為vol...

spring security多入口登入

專案中整合了ldap的驗證,從其他應用跳轉過來希望不進行二次登入。因為ldap的使用者名稱和密碼已經在登入中滅失,所以無法在應用中做再次提交驗證。幸好其他應用和專案是可以共享資料庫,所以用資料庫共享使用者資訊進行免手工登入驗證。具體是其他應用對資料庫表portal admin插入一條使用者登入請求,...

webpack 多入口配置

順著官網的操作,我們可以本地測試起我們的專案 npm run dev,首先我們要理解webpack打包主要是針對js,檢視下面生成的配置,首頁是index.html,模版用的index.html,入口檔案用的mian.js file build webpack.base.conf.js entry ...