非一致性記憶體訪問的讀寫鎖
原文作者:
- Irina Calciu Brown University [email protected]
- Dave Dice Oracle Labs [email protected]
- Yossi Lev Oracle Labs [email protected]
- Victor Luchangco Oracle Labs [email protected]
- Virendra J. Marathe Oracle Labs
- Nir Shavit MIT [email protected]
摘要
因為多核和多晶片計算機的快速增長,非一致性記憶體訪問(NUMA)架構在主流計算機系統中的地位越來越重要。為了在這些新系統上獲取最好的效能,我們需要重新修改當前程式組成部分的併發演算法和同步原語。這篇論文重新設計了很關鍵的同步原語——讀寫鎖。
據我們所知,本文論述的讀寫鎖是第一個專門為NUMA架構設計的,以及這個讀寫鎖的變種。這些變種考慮了在高併發讀的情況下的讀和寫的公平性以及在同一個NUMA節點上讓寫批量執行。我們的演算法利用群組所技術來處理寫者之間的同步,比較適合NUMA架構,同時使用二進位制標誌來協調讀者和寫者,利用簡化的讀者計數器,可以在NUMA上使讀者間取得更好的併發性。最經過我們的基準程式測試之後,這些簡單的NUMA鎖演算法竟然比目前比較出色的讀-寫鎖的效率還高出10倍以上。為了了評估在真實環境中我們演算法的效率,我們也提供了用基準程式 kccachetest測試的結果。Kccachetest屬於Kyoto-Cabinet 發行版,這是一個開源的資料庫使用了大量的 pthread 執行緒庫的讀寫鎖。跟最好的鎖比,我們的鎖也把kccachetest的效率提升了40%。
1.介紹
因為微處理器的廠商不斷的追求多核和多晶片系統(Intel的Nahalem 和Oracle Niagara平臺是比較典型的例子),計算機工業界正在快速採用分散式和快取一致性的NUMA架構。這些系統都包含有多個節點,每個節點都有自己的記憶體,快取和多核處理器。這些系統呈現了一個統一的程式設計模型,記憶體全域性可見和快取一致。在節點間的快取一致性通訊通道被稱為外部互聯。跟節點內的通道相比,這些節點間的鏈路一般延遲比較大且頻寬比較低。為了降低延遲和少用佔用互聯頻寬,NUMA策略鼓勵節點內通訊而不是節點間。
在NUMA架構上編寫一個高效的軟體是非常有挑戰的意見事情,因為這類系統對記憶體和處理器之間的關係抽象的比較初級和“扁平化”,為程式設計師隱藏了真是的拓撲模型。程式設計師必須參閱架構手冊和使用系統相關的庫函式才能知道系統的拓撲結構。不考慮NUMA的多執行緒程式會有不少效率問題,主要是節點間的記憶體一致性的通訊和節點間頻寬引起。而且節點間互聯通道是共享資源, 對其競爭和延遲,會導致有一個執行緒產生的快取一致性通訊會影響另外一個不相關的執行緒。在移植到NUMA架構之前,目前多執行緒程式中的併發資料和同步結構都需要仔細的設定。很重要的同步結構是讀寫鎖。
讀寫鎖放寬傳統互斥鎖的限制,允許多個讀執行緒同時擁有鎖。寫執行緒仍需互斥訪問。讀寫鎖的使用範圍非常廣泛,包括作業系統核心,資料庫,高階科學計算軟體,軟體記憶體事務中[6]。
讀寫鎖已經被研究了幾十年了[1,2, 11, 13–16],有簡單的計數器和訊號燈的方案[2],利用中心等待佇列[14,16],也有用類似可伸縮的非零指示器(SNZI)[15]等更高階的資料結構。在所有這些方案中,SNZI依賴於中心資料結構來協調各個執行緒,所以會遇到可升縮性問題[15]。SNZI演算法通過把讀者放到SNZI樹的葉子節點上來追蹤寫者(請求讀鎖的執行緒)。通過把SNZI樹的葉子節點分到不同的NUMA節點,讀執行緒就可以知道自己在NUMA上哪個節點了。而寫執行緒依舊忽略NUMA,所以會有伸縮性問題。
Hsieh,Weihl和Vyukov獨自提出了一種簡單分散式的方式來實現讀寫鎖。每個分散式的讀寫鎖包括N(N是系統處理器個數)個讀寫鎖。每一個讀者分配一個讀寫鎖,在執行讀臨界區之前必須獲取這把讀鎖。通過強制的加鎖次序來避免寫者之間的死鎖。這個方法可以用到NUMA架構上,把N限制為系統中NUMA節點的個數,每個讀者分配節點專屬的鎖。我們把這個變化了的演算法稱作DV,一定程度上感知了NUMA,跟SNZI讀寫鎖類似 。在沒有寫者的情況下,在各個節點上的讀者在獲取和釋放讀許可權時不會產生任何節點間的寫一致性互動。 然而每一個節點上的寫者在獲取寫許可權時都會潛在的產生大量的節點間一致性互動。所以隨著寫次數的增加,DV的效率也會顯著下降。由於使用加鎖次序來避免死鎖,在後加鎖的節點上的讀者比先加鎖節點上的讀者更有效率優勢,這是不公平的。
這篇論文展示了一組轉為NUMA設計的讀寫鎖,相比之前的讀寫鎖演算法具有更好的效能和伸縮性。我們從三方面入手來設計我們的鎖。第一,跟DV類似,我們維護了一個分散式的資料結構用來儲存讀者元資料,讀者通過更新他們節點的位置資訊來說明自己的意願。通過讀指示器來更新減少了節點間一致性的互動。第二,寫者優先把訪問許可權交給跟自己同一節點的其它已經阻塞的寫者,可以增強節點內部快取的鎖和臨界區中要訪問的資料區域性性。最後,我們的演算法為讀者和寫者維護了一個非常緊湊的執行路徑,減少了獲取和釋放操作帶來的延遲。
我們的讀寫鎖演算法基於目前剛開發的群組所技術,他允許建立NUMA的互斥鎖。簡單說,寫者使用群組所來同步同時維護寫者之間的互斥。使用群組所方式,寫者正在釋放的鎖會優先交給跟自己同節點的處於阻塞的寫者,因此可以減少鎖在節點間的轉移。
我們的讀寫鎖也包括了分散式的讀指示器,這個資料結構可以追蹤讀者。讀者獲取鎖時會在讀指示器中記錄,釋放鎖會從讀指示器中去除。寫者查詢讀指示器來發現併發的活躍讀者。由於讀指示器是分散式的,讀者只需要訪問他們所在節點的鎖的元資料就可以了。我們還額外使用了一個簡單的標識變數用來協調讀者和寫者。這個簡單的演算法是的NUMA上的讀寫鎖的效率遠比之前很出色的算好要好。
我們提供的不同的讀寫鎖可以用公平性這個屬性來區分。我們也展示了不同的“優先”策略:讀者優先,寫者優先,中性策略。讀者優先策略是儘快讓讀者獲取鎖,而不管到達的次序。而寫者優先策略對於寫者本身沒有偏向性。具體一點說,這些優先策略允許讀者和寫者在競爭鎖時比排在他們前面的寫者和讀者更早獲取鎖。除了中性策略之外的優先策略,會導致參與獲取沒有優先鎖的執行緒飢餓。我們通過允許一定的鎖機制改變優先策略來避免飢餓問題,飢餓的執行緒就可以繼續往前執行了。飢餓的執行緒變得沒有“耐心”了,短暫的改變了優先策略。
我們對我們的讀寫鎖進行了實證測試,自己相互之間的效能比較以及和先前的讀寫鎖進行比較。我們的測試是在 256路 4節點的 Oracle SPARC T5440 伺服器上進行的,我們的讀寫鎖的效能在各中型別的任務中都比要遠超之前的讀寫鎖。在我們自己的基準程式中,我們的鎖比之前最好的讀寫鎖(基於SNZI的ROLL鎖)的效能都要強10倍。
第二節討論了我們所的設計方法,第三節詳細論述了我們的鎖演算法。第四節使我們的實證測試結果,第五節是我們的結論。
2. 鎖設計原理
NUMA的互斥鎖已經有不少人深入研究過了。然後據我們所知,還沒有人研究過NUMA上的讀寫鎖。NUMA的互斥鎖只追求一個目標,就是減少鎖的遷移,這樣可以在節點內部保持鎖和臨界區域資料的區域性性。NUMA互斥鎖儘量減少寫非法和一致性失效的比例,快取的失效需要從另外的節點通過互聯通道傳輸過來。我們認為跟通常的讀寫鎖一樣,NUMA的讀寫鎖需要額外考慮儘量讓讀者之間高併發的執行。
我們發現這兩個目標很難兼顧,提升節點間讀者的併發性需要把鎖的元資料以及臨界區的資料分發到這些節點,而減少鎖的遷移需要減少這種分發。然而這種顯而易見的矛盾可以通過只在寫者間減少鎖的遷移,但同時讓讀者間更加高併發來解決。為了讓這個策略更加有效,我們必須儘可能讓併發的寫者來自同一個NUMA節點,以保持鎖在本地寫者之間傳遞。我們注意到聚集寫者的方式並不是完全不合適的。相反,它可以讓讀者間更好的併發,因為可以更好的在同一個節點處理鎖請求。我們用一個例子來說明這個設計的潛在好處。
Figure 1. 多核多晶片的NUMA例子。包括了2個晶片,每個晶片包括4個核。每個晶片是一個NUMA節點。每個核可以有多個硬體執行緒的上下文。每個核有它自己的L1 快取,晶片中的所有核共享一個L2快取。執行緒間的通訊通過本地快取(L1和L2)遠比通過遠端快取要快。因為後者需要通過互聯通道交換一致性訊息。在圖片中,執行緒 r1..r6是想要獲得讀寫鎖的讀鎖,執行緒w1..w6是要獲取寫鎖。
圖二 圖一的可能執行次序
圖一描述了一個NUMA系統中有6個執行緒想要對鎖L加讀鎖,6個執行緒想要對鎖L加寫鎖。我們假設有所保護的臨界區都訪問相同的資料。 圖二展示了使用不同的讀寫鎖,可能的讀者和寫者臨界區執行次序。圖二(a)是一種可能的臨界區執行次序,有初級的讀寫鎖(沒有聚集讀者或者寫者在一個節點上)來決定。這個排程次序顯示這種鎖沒有提供讀者間的高併發,因為它使用了更多的時間執行玩所有臨界區域。它造成了很多讀者的阻塞,高比例的在讀者和寫者之間切換降低了讀者間的併發性 。圖二(b)的排程策略使得讀者間的併發性更高。通過聚合讀請求,排程了一組讀者同時執行臨界區。然而在兩個不同節點裡的寫者交替執行,這導致了大量的一致性互動,使得寫者速度變慢。方框的寬度反應了臨界區的相對執行時間,更寬的方框顯示了節點間通訊的開銷。圖二(c)通過聚合在同一個節點內的寫者解決(b)的問題。結果寫者w2,w3,w5和w6在執行臨界區時減少了一致性失效。我們會在第四節看到這個會極大的提升我們所的效率。
3.讀寫鎖演算法
我們的讀寫鎖基於現有的群組所。我們的每一個讀寫鎖例項包括一個用來同步讀者的互斥鎖,我們通過群組所來解決讀者間的衝突。寫者為了互斥訪問必須先獲取這把群組所。在執行臨界區前,擁有群組所的寫者通過保證沒有併發的讀者在執行或者即將執行臨界區來保證沒有讀寫衝突。我們讀寫鎖的讀者使用分散式讀者指示器。讀者為了獲取讀寫鎖,必須修改讀者指示器。讀者指示器是一個分散式的計數器,每個NUMA節點一個計數器。每一個讀者加鎖時對本節點的計數器加一,釋放鎖是減一。最關鍵的是寫者只更新集中鎖,同時檢查讀者指示器而不會更新它。
本節我們描述了用群組所來實現讀者間的互斥,然後描述了三種讀寫鎖演算法,各自實現了三種優先策略(中性,讀優先,寫優先)中的一種。3.2到3.4簡要描述了這三個演算法。然後我們發現可以使用我們的讀寫鎖中的互斥鎖和讀者指示器來替換他們的實現。最後我們描述了我們讀寫鎖中實現的可伸縮性的讀者指示器。
3.1 寫者群組所
群組所是一項把NUMA無關的互斥鎖改變成NUMA相關的互斥鎖的技術。它使用了互斥鎖實現的兩個關鍵屬性,(1)佇列檢測,鎖的擁有者可以知道是否有執行緒正在請求鎖;(2)執行緒無關,鎖可以有一個執行緒來獲取但是由另外一個執行緒來釋放。
群組所是層次結構的,最上層有一把鎖,第二層有多把鎖,沒把鎖對應一個節點。最上層的鎖是執行緒無關的,第二層的鎖需要有佇列檢測功能。當一個執行緒擁有最上層的鎖時才能認為它擁有群組所。
為了獲得群組所,執行緒必須先獲取它所在節點的鎖然。在執行完臨界區之後,群組所的擁有者需要用本節點鎖的佇列檢測屬性來檢測是否有其它執行緒請求鎖,如果本節點有後續請求這,就把這本節點的鎖交給它。隨著本節點鎖的轉交,頂層鎖的擁有者也換成了這個後繼執行緒。如果鎖的擁有者沒有發現有其它執行緒請求鎖,那麼它就自己釋放鎖。頂層鎖的執行緒無關屬性在這裡就發揮作用了,鎖的擁有者可以從一個執行緒換成其它所在節點的其它執行緒,知道被其它某個執行緒釋放。為了避免飢餓和保證長期的公平性,群組所的實現中設定了一個最大鎖交換次數(在我們的實現中,我們把這個數字設定為64)。我們的演算法有意嚴格執行短期的 FIFO/FCFS的公平性用來提高總吞吐量。特別的,我們利用不公正性(允許獲取鎖的次序和申請鎖的次序不一樣)來達到減少鎖遷移次數和提高競爭執行緒的吞吐率。謹慎合理的使用非公平性可以減少一致性互動,提高快取命中率。
群組所的主要目的是減少節點間的一致性互動和一致性失效,這可以提高本節點cache的命中率。我們假設同一把鎖下的臨界區會引用相似的東西,請求鎖L很好的預示著有L保護的臨界區將會訪問剛剛有另外一個鎖L保護的臨界區訪問的資料。鎖的所有權轉移之後,將要被改寫的資料更有可能出現在本地快取當中,並且已經處於修改狀態,因為它可能被之前的執行緒修改了。因此這個臨界區的執行會比前面一個執行緒在另外一個節點中快。群組所減少了鎖的元資料和有鎖保護的資料在不同節點間遷移的次數。如果在另外的節點中快取行處於修改狀態,那麼現在他在本地快取中一定是非法或者不存在的。這個快取航需要從另外節點中同步過來,並且把狀態修改為共享。類似的,如果將要被改寫的快取行在本地快取中不是修改狀態,所有其他節點上的拷貝都會變成非法,如果快取行不是共享狀態,內容必須同步到寫者的快取中。讀者間的共享是唯一不需要一致性通訊的方式。我們比較少關心經典的NUMA問題,比如執行緒要訪問的記憶體是否線上程旁邊,更關心快取中是否有共享資料以及它的狀態。群組所儘量滿足遠端快取資料以可以減少寫非法和一致性失效,但不能解決遠端的容量,衝突和快取為命中。
在我們的讀寫鎖使用中,我們開發新的群組所,經典的ticket鎖用來做NUMA節點的鎖,分割槽的ticket鎖用來做頂層鎖 。我們稱它為C-PTL-TKT(Partitioned-Ticket-Ticket 群組所的簡稱)。我們公開了一個新的介面(isLocked),讀者通過這個介面來判斷是否寫鎖已經被申請。這個函式是通過比較分割槽ticket鎖中的請求和授權的序號來實現的。我們推薦在我們的讀寫鎖中使用C-PTL-TKT,它可以節省節點管理的開銷但仍然提供本地自旋。頂層和節點鎖都是FIFO的,儘管C-PTL-TKT並不需要FIFO。
3.2 中性鎖
圖三 中性鎖(C-RW-NP)。上半部分有讀者回字形,下半部分有寫者執行。 虛擬碼簡要的展示了鎖的獲取,臨界區的執行和鎖的釋放。在獲取鎖的階段,讀者和寫者獲取群組所,而讀者還需要設定讀者指示器,設定讀者指示器是原子操作。
我們把中性鎖簡稱為C-RW-NP(Cohort, Read-Write, Neutral-Preference),保證了讀者寫者之間的公平性。這裡的公平性是指讀者對於寫者,或者寫者對於讀者沒有任何優先,都是平等的。因此所有讀寫執行緒都從集中的群組所通過,這種方式在以前就在用了。圖三的虛擬碼簡要描述了C-RW-NP鎖。每個執行緒必須先獲取群組所。讀者使用群組所來獲得者是讀者指示器的許可權(具體實現在3.6節),然後釋放群組所並執行臨界區域。讀者在釋放群組所後再執行臨界區可以使讀者之間的併發性更大。寫者在獲取群組所之後,必須確保沒有讀者。這個通過在讀者指示器上自旋(行9-10)來等待所有讀者離開臨界區。這個演算法非常的清楚簡單,並且保證是中性的,因為讀者和寫者都需要獲取群組所。然而,要求讀者獲取群組所降低了C-RW-NP鎖的伸縮性,同時也增加了讀者獲取鎖的延遲。C-RW-NP鎖保留一部分快取區域性性,包括訪問鎖的元資料和臨界區訪問,因為所有操作都要通群組所。
我們注意到C-RW-NP鎖並不保證FIFO,獲取鎖的次序有群組所的策略鎖決定。
3.3 讀者優先鎖
圖四 讀者優先鎖
從上面可以看到,C-RW-NP有一個很大的缺陷,因為他需要讀者獲取群組所。對讀者來說請求群組所需要額外的開銷,即使群組所是非競爭的。對集中鎖的競爭會產生額外的一致性互動,從而對節點間互聯通道的競爭,雖然通過群組所的方式可以一定程度上減少節點間一致性互動。然而在臨界區訪問路徑上需要序列訪問群組所還是會成為伸縮性的瓶頸 。演算法通過群組所把讀者和寫者請求通過群組所來控制在很大程度上限制了讀者之間的併發性。最壞的情況下,甚至會沒有讀併發的存在 ,讀者和寫者交替執行。我們的讀優先鎖(C-RW-RP)解決了這兩個問題。
直觀上來說把讀者請求鎖集中起來可以最大化讀者之間的併發性,以此可以帶來更好的伸縮性。但這需要讀者優先於正在等待鎖的寫者。Courtois et al早期對讀寫鎖的研究已經對此有所研究,後來者的工作一直在公平性和伸縮性之間進行平衡和選擇。我們的C-RW-RP演算法也繼承了這種平衡。
圖四的虛擬碼描述了C-RW-RP鎖的實現。讀者和寫者之間的互動方式讓人人回想起經典的Dekker鎖,這種鎖每個執行緒都需要讓別的執行緒知道,然後在檢查其它執行緒的狀態。為了檢測和解決衝突,讀者必須對寫者可見,寫者必須對讀者和寫者同時可見。C-RW-RP的讀者無須獲取群組所,相反他們直接設定鎖的讀者指示器(行4)。然而每一個讀者只有在沒有寫者的情況下才能往前執行。然後讀者可執行他們的臨界區,並通設定讀者指示器釋放鎖。
寫者首先獲取CohortLock(行12),然後確認現在沒有併發的處於“活躍”的讀者。如果有併發的讀者(讀者指示器會顯示),寫者釋放CohortLock(行13-14)並等待所有的讀者完成(行15)。注意如果寫者僅僅等待讀者完成,但讀者卻源源不斷的到達,那麼寫者就有餓死的風險了。為避免餓死,我們引入了一個讀者柵欄(RBarrier),寫者可以使用它暫時阻塞新的讀者獲取C-RW-RP鎖。行(17-20)顯示了寫者使用柵欄(行23結束柵欄),行2-3顯示了柵欄阻塞了新的讀者。讀者柵欄用一個集中的計數器來實現。寫者會先等待事先確定的一個時間(行17),然後就不會在等待了。寫者的等待時間比較長,所以柵欄不會經常被使用,而且我們也不希望柵欄成為競爭的瓶頸。在我們的實驗中寫者的等待時間是1000迴圈等待。這個等待時間是可以調優的。在寫者使用柵欄後,所有的讀者會慢慢的執行完,當所有讀者執行完後,寫者就會執行它的臨界區,最後通過釋放CohortLock來釋放寫許可權。
儘管上面的演算法很簡單,但它有一個很大的效能問題,因為讀者和寫者之間的競爭互動和CohortLock的策略。考慮這種執行邏輯,有N個寫者 W1,W2, W3…, Wn等待在群組所上。W1是鎖的擁有者,但他還沒有到大行13。這意味著在第五行的isLocked 函式返回true,阻塞了所有的讀者。那個時候更多的讀者到達了,自動增加了讀者指示器,然後自旋等待isLocked函式返回false。下一步,W1執行 行13,發現有併發讀者,在行14釋放了群組所。同時,W1把鎖交給了W2,W2又把鎖交給了W3,如此往復。在這個過程中,群組所一直處於加鎖狀態,儘管鎖的擁有者一直在變,對所有呼叫isLocked函式的讀者來說,返回值一直是true。群組所擁有者在讀者間迴圈改變,減少了不必要的一致性互動,也減少了讀者的等待時間。在我們的實驗中我們觀察到這種在讀者和寫者間不必要的互動極大的降低了效率。而且迴圈還避免了加在寫者上的次序問題。
為了避免這個問題,我們為C-RW-RP增加了一個新的WActive變數,來反映群組所的邏輯狀態。我們修改了讀者和寫者間衝突檢測邏輯,把圖四 行5改成當WACtive true時自旋。同時,寫者的程式碼(行11至行21)改成如下程式碼
寫者獲取群組所然後進入迴圈。迴圈一開始就等待讀者指示器指示此時沒有等待或者活躍的讀者,如果長時間等待後也可以設定RBarrier。讀者指示器指示沒有活躍讀者時,程式碼會把WActive設定成true,然後驗證沒有活躍或者等待的讀者。如果情況是這樣的,就結束迴圈,寫者去執行自己的寫臨界區。如果讀者指示器指示有讀者,那麼寫者吧WActive設定成false,又從迴圈最開始執行,等待讀者執行完畢。寫者在等待讀者執行完畢時,仍舊持有鎖,這個可以避免在寫者間交換鎖帶來的效能損失。寫者執行完臨界區之後,把WActive設定成false並釋放群組所。讀者只能在一個很小的時間視窗下才能阻塞寫者,這個時間視窗就是寫者在檢測到有等待的讀者時重置WActive那個時間點。我們把它稱為C-RW-RP-opt。注意WActive只在群組所的保護下才能修改,當群組所被加鎖時WActive為tru,否則為false。沒有類似寫優先 “opt”,因為讀者可以快速解除他們的加讀鎖的意願,讓寫者優先。
3.4 寫優先鎖
傳統觀點認為讀優先策略的表現會比寫優先和中性策略要好。因為應用程式開發人員選了讀寫鎖而不是互斥鎖,我們可以期待工作負載中讀是佔大部分的。直覺告訴我們把讀者聚集起來可以獲得更好的讀者間併發性,以此獲取更好吞吐率。儘管我們相信直覺,讀寫鎖大多數情況下是加讀鎖較多,但我們也認為寫優先策略間接也得獲得相同的結果——把讀者聚集起來。這是因為寫優先策略會讓很多讀者等待,當所有讀者完成臨界區後大量讀者可以併發同時執行。更進一步,我們也觀察到讀者優先策略會導致效率下降,在4.3節有描述,極大的降低了鎖的潛在可伸縮性。
圖五顯示了寫優先鎖的虛擬碼,我們稱它為 C-RW-WP。我們的C-RW-WP演算法跟我們的C-RW-RP演算法完全對稱,唯一的區別寫者程式碼變成了讀者程式碼 ,讀者的程式碼變成了寫者程式碼 。讀者設定讀者指示器(行4),檢查寫者(行5),如果有寫者,讀者重置讀者指示器並且等待寫者執行結束。如果讀者等待時間過長了(在我們的實驗中是1000次迴圈),可以設定寫者柵欄(行10)來阻塞新的寫者獲取群組所(行18-19)。寫者首先確認寫柵欄沒有被設定(行18-19),然後獲取群組所(行20),並且在執行臨界區錢保證沒有併發的讀者(行21-22)。
3.5讀寫鎖概述
我們可以觀察到我們的讀寫鎖不需要知道讀者指示器和互斥鎖的實現。我們的讀寫需要讀者指示器提供到達,離開和isEmpty操作,互斥鎖需要提供獲取,釋放和isLocked操作。任何支援這些操作的讀者指示器和互斥鎖都可以在我們的演算法中使用。我們希望只要經過一點小的修改,支援這幾種操作的讀者指示器和互斥鎖就可以使用。
我們的讀寫鎖設計靈活,可以極大的方便程式設計師建立一個適合他們應用程式的讀寫鎖。例如,本論文提出了一種NUMA的讀寫鎖就是利用了NUMA的互斥鎖和可伸縮性的讀者指示器。在我們的實驗中(第四節),我們也展示了讀寫鎖的效能,這個讀寫鎖使用了分散式的計數器作為讀者指示器,MSC鎖作為讀者建的互斥鎖。這種鎖適合那些寫比較少的應用程式。
3.6 追蹤讀者
Lev et al.發現讀寫鎖的讀者可以用讀者指示器來追蹤。寫者檢查是否有讀者時不需要讀者的個數,僅僅需要知道是否讀者。
這個讀者指示器可以用一個自動更新的計數器來實現,記錄正在執行或者想要執行臨界區的讀者個數。然後,一個簡單的計數器無法再NUMA系統上做到可伸縮。觀察到這個現象後, Lev et al. 提議了一個基於SNZI的讀寫鎖。基於SNZI的方案讓讀者指示器有了極大的可伸縮性,儘管演算法複雜和讀者在中低等的競爭中引入了開銷(第四節有介紹)。結果,我們採用了一個簡單的策略,我們把一個“邏輯”計數器分成了多個物理計數器,每個NUMA節點一個計數器。我們方法的主要目的是在讀者比例不是很高時有比較少的而延遲,當有很多讀者是,又有好的伸縮性。
一個讀執行緒總是操作它所在節點的讀者指示器。這保證了計數器的修改不會產生節點間的一致性互動。然而,當獲取群組所後,寫者必須檢查所有讀寫鎖的讀者指示器以保證可以安全的執行臨界區。這個增加了讀者的開銷。這裡有一個非常清楚的權衡,假設讀寫鎖大部分是加讀鎖的,我們通過讓寫者的執行路勁變長從而簡化了讀者的執行路徑(僅僅是對本節點讀者計數器做自增)。目前大多數的多核和多晶片系統只有很少幾個NUMA節點(我們實驗中用到的是4個),所以我們相信讀者增加的開銷不會成為主要的效能問題。將來NUMA系統的節點數變多,這個就有可能成為問題了,但是我們把這個解決方案留到以後解決。
分散的計數器有幾種不同的實現方式。我們這裡討論兩種方法。第一,每個節點一個整型計數器。每一讀者在獲取鎖時原子的增加本節點的計數器,在釋放鎖時減少本節點計數器。使用對齊和填充,每一個節點的計數器被放在各自的快取行中來避免偽共享。當寫者獲取鎖時,要確認每一個節點的讀者計數器為0,如果非0,寫者通過自旋等待。
簡單的分散式計數器的方法減少了節點間的一致性互動,但是還是有節點內的競爭。我們的第二個方法就是減少這個節點內的競爭,該方法為每一個節點引入了一對 進和出的計數器。讀者在獲取鎖時增加ingress計數器,釋放鎖時增加egress計數器。通過把本節點計數器分為兩個變數,我們把讀者到達和離開的競爭分成了兩個部分。在一個節點上,到達的執行緒可以獨立的更新ingress變數,而同時離開的執行緒可以更新egress變數。Ingress和egress變數可能在同一個快取行中。當ingress和egress相等時,邏輯上可認為讀者計數器為0。有趣的是使用單一計數器或者使用 ingress-egress 計數器,效能上都要比SNZI的讀者其實好,至少在我們的測試平臺是這樣的。我們相信這個好效能是平臺相關的。如果有一個很多節點的系統,那麼寫者掃描這些節點需要大量的工作,用這種方式來解決讀者和寫者間的衝突就不可能了。但目前的平臺上,ingress-egress計數器是我們推薦的實現。
當獲取鎖時,寫者檢查每一個節點的上的 ingress和egress的值是相當等。這無法自動完成,需要非常小心的去避免和正在修改技術的讀者出現競爭。具體的說,在我們的C-RW-WP演算法中,寫者必須先讀取egress變數,然後是ingress變數來爬段是否相等。注意這兩個計數器都是單調遞增的,必須永遠保證egress小於等於ingress。
4.實驗評估
我們展示了我們的NUMA讀寫鎖的評估結果,以及和其它有名的讀寫鎖的比較。通過特意編寫的微小基準程式演示了這些所的伸縮性結果。我們的結果覆蓋了非常廣泛的配置,不同臨界區和非臨界區的長度,只讀和讀寫的臨界區等。我們的報告也揭示了讀優先具有好的效能。所以我們展示了讀指示器的實現是如何影響我們讀寫鎖的伸縮性的。最後我們展示了kyoto-cabinet開源資料庫包中的 kccahe-test基準程式的測試結果。我們實驗的結果表明我們的NUMA鎖的效能要遠好於其它讀寫鎖。
我們展示了我們所有鎖演算法的測試結果:C-RW-NP, C-RW-NP和C-RW-NP-opt, 以及C-RW-WP。除非特別說明,我們用 ingress-egress計數器作為我們讀者指示器的實現。我們比較了基於SNZI的ROLL鎖,分散式讀寫鎖(DV),以及最近有 Shirako et al釋出的NUMA無關的讀寫鎖。因為我們的鎖是基於群組所的,我們增加了一個簡單的互斥鎖以便理解我們讀寫鎖的優點。最後,為了量化在我們的讀寫鎖中使用群組所的好處,我們在C-RW-WP中使用MSC鎖來比較他們的效能差異。我們把這個教唆 DR-MSC鎖。
我們用C實現了所有以上演算法,用GCC4.4.1 選項O3 在上32位機上編譯。測試在Oracle T5440系列的系統上進行,他有4個Niagara T2+ Sparc的晶片,每個晶片包括8個核,每個核有2個流水線,每個流水線有4個執行緒上下文,總共有256執行緒上下文,處理器頻率為1.4GHZ。每個晶片都有自己本節點的記憶體,4MB的L2快取,每個核有8KB的L1資料快取。每一個T2+晶片都是一個NUMA節點,這些節點有一箇中心一致性hub連線。Solaris 10的排程器是會儲存工作和維護快取的,儘量避免執行緒遷移。在我們的測試中,執行緒遷移很少發生。顯示的加入記憶體柵欄是非常有必要的,這在我們的虛擬碼裡沒有看到。
我們用LD_PRELOAD 庫來實現以上的鎖演算法,這個庫暴露了標準的POSIX pthread_rwlock_t變成介面。這允許我們通過改變LD_PRELOAD的實現來改變鎖的實現方式而不用修改應用程式程式碼。
我們試用Solaris的 schedctl介面去查詢執行緒執行在哪個CPU上,在SPARC和X86只需兩個指令。然後把CPU的數量轉換成NUMA的節點數量。在我們的實現中,執行緒在獲取鎖時都會去查詢一下NUMA節點的數量。我們記錄那個數字,並保證讀者從同一個節點離開。在x86平臺上,RDTSCP指令可以替代schedctl用來返回CPUID。
4.1 RWbench
為了瞭解我們鎖的效能特性和與其他鎖比較,我們實現了一個多執行緒的基準程式,這基準程式重複不斷的執行臨界區。這個基準程式叫RWbench,這個一個靈活的框架,可以讓我們測試各種任務,臨界區和非臨界區的長度,不同分佈的讀寫操作,不同的快取訪問等。所有這些可以通過配置變數來做到。它使用pthreads庫的讀寫鎖介面去獲取和釋放所,我們使用LD_PRELOAD庫來選擇鎖的實現。
RWBench可以根據配置建立併發執行緒,每個執行緒迴圈執行10秒。在最外城的迴圈開始出,使用伯努利隨機數來決定這個迴圈是讀臨界區還是讀寫臨界區。選擇的讀寫臨界區的概率可以用變數來配置。臨界區修改一個共享的64個整數的陣列,有一個全域性的讀寫鎖來保護。只讀操作會迴圈RCSLen次(可配置),每個迴圈會從共享陣列中取出兩個整數。讀寫操作迴圈WCSLen次(可配置),每個迴圈去除兩個整數,對一個整數做加法,對另外一個整數做減法。主迴圈中的非臨界區更新執行緒私有的64個整數 NCSLen次。基準程式結束後,確認整個陣列的和是0。
基準程式在10秒後給出了吞吐率,以工作執行緒執行的最上層迴圈次數來表示。我們為每個配置執行呢3次,去平均值。觀察的變化非常小。為了更貼近真實世界的執行環節,我們沒有特意把執行緒跟硬體上下文繫結,而是依賴於Soloaris核心的排程器。不像其它NUMA鎖,我們的鎖無需繫結。
4.2 RWBench 可伸縮性測試
圖六 RWBench的可伸縮性測試,讀寫分佈為:2%,5%,10%和20%的寫請求。所有的圖是對數座標。RCSLen=4, WCSLen=4,NCSLen=32。X軸為為執行緒數量,從1到255。Y軸是吞吐率,以每秒百萬次迴圈來表示。
圖六是RWBench的測試結果,包括不同的鎖,不同的讀寫比列分佈。我們認為測試中的讀寫分佈已經覆蓋了現實中很多應用程式的情況。我們也收集了寫佔50%時的資料,測試結果跟圖六(d)相似。在這些實驗中,我們有意使用小的臨界區和非臨界區,這樣可以使我們更好的瞭解在高頻率加鎖的情況下的讀寫的行為。
首先,C-RW-WP是所有的鎖中效能最好的。有趣的是DR-MCS在2%寫的情況下效能第二,但隨著寫負載的增加效能急劇下降。這是因為寫在效能中所佔比列越來越高,DR-MCS存在過度的一致性互動,因為MCS鎖不關心NUMA架構。DV線上程少的時候比較有競爭力,但是在競爭多時效能下降很快,大概是因為寫者需要獲取所有NUMA節點的讀寫鎖,這會導致大量的一致性互動和讀寫時獲取鎖的延遲。
ROLL鎖隨著執行緒數量的增加有一定的伸縮性。這是因為在我們測試中的執行緒被Solaris的排程器分散在整個系統中 ,結果只有很少的執行緒被固定在SNZI樹的葉節點上。最終測試結果是很多執行緒很容易“爬”上SNZI樹,在根節點上競爭,這個根節點是全域性共享的會鄭家一致性互動。ROLL在很多執行緒的情況下可升縮性更好些,大概是因為SNZI數的葉節點上的負載足夠減少訪問根節點的執行緒數量,因為減少了整個系統的一致性互動。然後在20%寫負載的情況下伸縮性就收到了限制,寫者的NUMA無關性開始扮演可伸縮性的重要角色。另外一個NUMA無關的Shirako鎖,在我們NUMA系統上沒有什麼伸縮性。
群組展示了又去的效能特點。因為他不提供任何讀者間的併發性,群組的可伸縮性還不如最好的讀寫鎖。然後鎖著寫者的增多,群組的效能開始和和我們其他的讀寫鎖接近了,除了C-RW-WP在20%寫的情況下,它跟其它鎖相比也是有競爭力的。這說明了甚至在寫較多的情況下,寫者間的互斥部分會成為影響可伸縮性的重要因子,因此群組在NUMA系統上非常高效和伸縮性。這個跟其它鎖比較來也是有競爭力的。
相對於簡單的互斥鎖,讀寫鎖通常有更長的執行路徑(延遲)和訪問更多的共享元資料。後者可以降低伸縮性。把群組和真正的讀寫鎖比較非常有趣,因為潛在的讀者間的併發性無法彌補額外的來自讀寫鎖的開銷,特別當臨界區非常小或者執行緒數量很少時。C-RW-NP只有在寫負載為2%時,才可以是讀者間的併發性很高。在所有其它情況下,它和群組類似,因為讀者為了設定讀者指示器也需要獲取群組鎖。
最後,C-RW-RP和C-RW-RP-opt之間的效能區別也清楚的展示了C-RW-RP寫許可權迴圈轉移的缺陷。然後C-RW-RP-opt的可伸縮還不如C-RW-RP,因為它的讀優先效能有問題,這會在下面描述。
4.3 讀優先效能分析
從圖六可以清楚的看到,我們的讀者優先鎖的效能遠比寫優先鎖差。眾所周知嚴格的讀優先策略有可能讓寫者餓死。然而,我們觀察到讀優先引起的第二個現象——讀者無法充分的併發執行。
我們有固定數量的執行緒,隨機決定對讀寫鎖是加讀鎖還是寫鎖。 我們假設讀會比寫多,訪問讀寫鎖中是讀占主導地位。我們假設大多數執行緒是讀的。過一段時間這些讀執行緒會完成他們的操作,有一部分會變成寫執行緒,但大多數仍然是讀執行緒。這些寫者會阻塞在柵欄上,讓讀執行緒優先。最後當沒有活躍的讀者時,寫者才允許執行。但當寫者完成他的操作,很快讀者就會再次執行,因此阻塞了大量的寫者。這個不受歡迎的模式可以一直持續。在任何時候,只要有一個寫執行緒或者幾個活躍狀態的讀執行緒,都會使獲取寫許可權的執行緒阻塞。這個導致為充分利用讀者間的併發性,致使系統未充分利用。儘管我們的負載是以讀為主,我們有足夠多執行緒和讀優先的鎖策略,我們的讀者吞吐率還是很低減少了讀者的併發性。由於讀優先引起寫飢餓導致寫者無法變成讀者,也限制了讀者建的併發性。在RWBench中我們也觀察到這個現象,我們觀察到伺服器執行緒查詢和更新有讀寫鎖保護的資料結構 。
有人可能會懷疑寫優先策略是否也有類似的問題。儘管寫者會阻塞讀者,我們不認為寫優先策略的問題同樣嚴重。我們假設讀寫鎖絕大部分時間處於讀模式中。因此,即使一批寫者阻塞了併發的讀者,這些執行緒最終會請求讀鎖。結果讀者只是停頓了一小會兒。一旦寫者執行完畢,一組等待的讀者可以併發執行了。
嚴格的 讀/寫優先策略會導致請求不是優先鎖的執行緒阻塞。因此,為了避免這種情況,我們認為一個通用的讀寫鎖演算法需要檢測有優先策略導致的飢餓,並且從飢餓中恢復過來。如果是寫優先策略,如果讀者等待時間太長了,可以用鎖阻塞即將到來的寫者,讓等待中的讀者先執行。事實上這種機制可以把讀者聚集起來執行,產生更大的讀者併發。更有效的是鎖可以無縫的從寫優先轉換成中性的或者讀優先。同樣的,如果是讀優先,寫者阻塞很長時間後,寫者把鎖轉換成中性的或者寫優先的。這個機制在圖五反飢餓中有闡述。我們用柵欄來阻塞寫者,在圖三中中性策略的“沒耐心”的讀者請求群組鎖。讀者首先使用最快的執行路徑,但是如果他們無法繼續向前執行了,他們就使用差一點的執行路徑去獲取群組鎖。這個方法的效率也還可以。
4.4 讀者指示器實現
就如第3.6節描述的,有很多種方式可以實現讀者指示器。我們來比較一下我們所討論過的三種實現方式的效能,整型計數器,一個節點一個整型計數器,每個NUMA節點一個 ingress-egress變數對。圖七顯示了用這些計數器的C-RW-WP的效能,其中讀者佔98%寫者佔2%。C-RW-WP-1RC代表了C-RW-WP用一個計數器實現。這個計數器明顯是一個伸縮性的瓶頸,當執行緒數增加時會導致了極大的效能損失。C-RW-WP-4RC是C-RW-WP用了4個計數器,每個節點一個計數器。改進版的分散式計數器極大的提升了C-RW-WP的效率(用了4對ingress-egress變數對,一個節點一對)直至有96個執行緒,之後由於單個節點上競爭,效能開始下降。Ingress-egress降低了一半的競爭,伸縮性好於多個計數器的實現。
C-RW-WP的可伸縮性效能還能提高一點,通過基於節點的計數器和讀者指示器來提高NUMA節點的區域性性。C-RW-WP-Shuffle 是C-RW-WP的變種,它隨機派發讀者指示器給相應的執行緒。訪問計數器的執行緒個數保持相等,但是有些執行緒可以在不同節點執行。我們從圖中可以看到C-RW-WP中很大一部分效能改善來自NUMA的區域性性。
4.5 kccachetest效能
Kccachetest由kyoto-cabinet釋出包提供,是一個流行的開源資料庫包。它是一個測試記憶體資料庫的基準程式。CacheDB 大量使用了標準的pthread_rwlock_t操作,基準程式的效能對於鎖的實現非常敏感。我們用刻薄的引數來執行程式,建立了一個沒有持久化的記憶體資料庫,然後隨機選擇了一些事務執行在那個資料庫上。基準程式和資料庫處於同一個程序中,通過呼叫和共享記憶體來通訊。基準程式被限制成最多具有64個執行緒。執行緒資料可以在命令列說說明,每一個工作執行緒迴圈選擇一個操作執行在資料庫上。有些情況下,僅僅是一些查詢,刪除操作,另外一些則是複雜的事務。每一個執行緒完成相同數量的操作,程式會顯示出第一個執行緒開始執行到最後一個執行緒結束的時間間隔。不幸的是關鍵字範圍的大小和記憶體資料庫的資料是一個執行緒的函式。所以我們增加執行緒數量時會同時增加關鍵字範圍和資料,這意味著不同執行緒數的結果很難互相比較。因此我們沒有對kccachetest的結果畫圖,只做了一張表格。
圖八顯示了用不同的讀寫鎖時kccachetest的效率。當執行緒少時,所有的鎖都有競爭力,當執行緒數量增加時,他們的效率變化就大不一樣了。Kccachetest有一系列的臨界區組成,包括短的只讀和讀寫臨界區,長的複雜的讀寫臨界區。總的來說,負載中讀寫臨界區佔大部分,會請求寫鎖。結果,群組所的效能和我們的NUMA讀寫鎖差不多,比其他NUMA無關的鎖要好很多,DR-MCS,DV,ROLL以及Shirako。DR-MCS伸縮性很差,因為寫者請求MCS鎖時會強制鎖和資料的快取行在節點間同步的次數比其它鎖多。因為群組鎖減少了鎖的遷移,所以它的效能好點。我們的NUMA讀寫鎖,除了C-RW-RP之外,進一步擴充套件了群組的優點。C-RW-RP的效能優於擁有者的轉移而效能不好,結果就是可伸縮性不如其它鎖。然而她確實比其他先前的鎖都要好。總的來說,C-RW-WP和C-RW-NP的效能最好,比最好的讀寫鎖(DV和Shirako)要好40%。
5. 結論
多核和多晶片系統的快速發展使NUMA架構變得普遍,基礎的資料結構和同步願意必須重新設計才能適應這些新環境。我們介紹了一些列驚人的簡單NUMA讀寫鎖,我們的鎖比先前的鎖效能要提高很多。寫者使用中央鎖元資料,讀者使用分散的元資料。基準程式表明我們表現最好的鎖比之前最好的鎖要快10倍以上。在真實應用程式(kyoto cabinet資料庫)用了我們的鎖之後,效率提升了40%。