簡體   English   中英

無鎖多線程適合真正的線程專家

[英]Lock-free multi-threading is for real threading experts

我正在閱讀Jon Skeet對一個問題的回答,他在其中提到了這一點:

就我而言,無鎖多線程適用於真正的線程專家,我不是其中之一。

這不是我第一次聽到這個,但我發現如果你對學習如何編寫無鎖多線程代碼感興趣,很少有人談論你實際上是如何做的。

所以我的問題是除了學習所有關於線程等的知識之外,你還從哪里開始嘗試學習專門編寫無鎖多線程代碼以及有哪些好的資源。

干杯

當前的“無鎖”實現大部分時間都遵循相同的模式:

  • 讀取一些狀態並復制它*
  • 修改副本*
  • 進行聯鎖操作
  • 如果失敗重試

(*可選:取決於數據結構/算法)

最后一點與自旋鎖非常相似。 事實上,它是一個基本的自旋鎖 :)
我同意@nobugz 的觀點:無鎖多線程中使用的互鎖操作的成本主要由它必須執行的緩存和內存一致性任務決定

然而,使用“無鎖”數據結構所獲得的是“鎖”是非常細粒度的 這減少了兩個並發線程訪問同一個“鎖”(內存位置)的機會。

大多數時候的訣竅是你沒有專用的鎖——相反,你將例如數組中的所有元素或鏈表中的所有節點視為“自旋鎖”。 如果自上次閱讀以來沒有更新,您可以閱讀、修改並嘗試更新。 如果有,請重試。
這使您的“鎖定”(哦,抱歉,非鎖定 :) 非常細粒度,而不會引入額外的內存或資源要求。
使其更細粒度會降低等待的可能性。 在不引入額外資源需求的情況下使其盡可能細粒度聽起來很棒,不是嗎?

然而,大部分樂趣可以來自確保正確的加載/存儲排序
與直覺相反,CPU 可以自由地對內存讀/寫重新排序——順便說一下,它們非常聰明:您將很難從單個線程中觀察到這一點。 但是,當您開始在多個內核上進行多線程處理時,您會遇到問題。 你的直覺會崩潰:僅僅因為指令在你的代碼中較早,並不意味着它實際上會更早發生。 CPU 可以無序處理指令:他們特別喜歡對具有內存訪問的指令執行此操作,以隱藏主內存延遲並更好地利用其緩存。

現在,可以肯定的是,代碼序列不會“自上而下”流動,而是像根本沒有序列一樣運行 - 並且可以稱為“魔鬼的操場”。 我認為對於將發生什么加載/存儲重新排序給出確切的答案是不可行的。 相反,人們總是說在玉米方面,並不妨易拉罐做最壞的打算。 “哦,CPU可能會將該讀取重新排序到該寫入之前,所以最好在此處放置一個內存屏障,在這個位置。”

事情是由事實,即使這些玉米不妨可以在CPU架構不同復雜。 可能是這種情況,例如,一些是保證沒有一個架構發生可能發生在另一個上。


要正確使用“無鎖”多線程,您必須了解內存模型。
然而,獲得正確的內存模型和保證MFENCE ,正如這個故事所證明的那樣,英特爾和 AMD 對MFENCE的文檔進行了一些更正,這在 JVM 開發人員中引起了一些騷動 事實證明,開發人員從一開始就依賴的文檔並不那么精確。

.NET 中的鎖會導致隱式內存屏障,因此您可以安全地使用它們(大多數情況下,也就是說……參見Joe Duffy - Brad Abrams - Vance Morrison在延遲初始化、鎖、易失性和內存方面的偉大之處)障礙。:)(請務必點擊該頁面上的鏈接。)

作為額外獎勵,您將在支線任務中了解 .NET 內存模型 :)

還有來自 Vance Morrison 的“oldie but goldie”: What Every Dev Must Know About Multithreaded Apps

...當然,正如@Eric提到的, Joe Duffy是關於這個主題的權威讀物

一個好的 STM 可以盡可能接近細粒度鎖定,並且可能會提供接近或與手工實現相當的性能。 其中之一是STM.NETDevLabs項目MS的。

如果您不是只使用 .NET 的狂熱者, Doug Lea 在 JSR-166 中做了一些很棒的工作
Cliff Click對哈希表有一個有趣的看法,它不依賴於鎖條——就像 Java 和 .NET 並發哈希表那樣——並且似乎可以很好地擴展到 750 個 CPU。

如果您不害怕涉足 Linux 領域,以下文章將提供有關當前內存架構內部結構以及緩存行共享如何破壞性能的更多見解:每個程序員都應該了解的內存知識

@Ben 對 MPI 發表了很多評論:我真誠地同意 MPI 可能會在某些領域大放異彩。 與試圖變得智能的半生不熟的鎖定實現相比,基於 MPI 的解決方案可以更容易推理、更容易實現且不易出錯。 (然而 - 主觀上 - 對於基於 STM 的解決方案也是如此。)我還敢打賭,正如許多成功的例子所表明的那樣,在例如 Erlang 中正確編寫一個像樣的分布式應用程序要容易幾光年。

然而,當 MPI 在單核、多核系統上運行時,它有其自身的成本和問題。 例如在 Erlang 中,圍繞進程調度和消息隊列同步有一些問題需要解決。
此外,在其核心,MPI 系統通常為“輕量級進程”實現一種協作N:M 調度 例如,這意味着輕量級進程之間不可避免地存在上下文切換。 確實,它不是“經典的上下文切換”,而主要是用戶空間操作,並且可以快速進行 - 但是我真誠地懷疑它是否可以將其置於聯鎖操作所需20-200 個周期內 即使在 Intel McRT 庫中,用戶模式上下文切換肯定也較慢 使用輕量級進程進行 N:M 調度並不新鮮。 LWP 在 Solaris 中存在很長時間了。 他們被遺棄了。 NT中有纖維。 他們現在大多是遺物。 NetBSD 中有“激活”。 他們被遺棄了。 Linux 對 N:M 線程有自己的看法。 它現在似乎有些死了。
不時有新的競爭者出現:例如來自英特爾的 McRT ,或者最近的User-Mode Scheduling與來自 Microsoft 的ConCRT
在最低級別,它們執行 N:M MPI 調度程序所做的工作。 Erlang - 或任何 MPI 系統 - 可能會通過利用新的UMS使 SMP 系統受益匪淺。

我想 OP 的問題不是關於任何解決方案的優點和主觀論據,但如果我必須回答這個問題,我想這取決於任務:用於構建運行在具有多核的單個系統,無論是低鎖定/“無鎖定”技術還是 STM 都將在性能方面產生最佳結果,並且在任何時候都可能在性能方面擊敗 MPI 解決方案,即使上述問題得到解決例如在 Erlang 中。
為了構建在單個系統上運行的任何稍微復雜的東西,我可能會選擇經典的粗粒度鎖定,或者如果性能非常重要,則選擇 STM。
對於構建分布式系統,MPI 系統可能是一個自然的選擇。
請注意, .NET也有MPI 實現(盡管它們似乎不那么活躍)。

喬·達菲的書:

http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html

他還寫了一篇關於這些主題的博客。

正確使用低鎖程序的訣竅是在深層次上准確理解內存模型的規則在您的硬件、操作系統和運行時環境的特定組合上是什么。

我個人還不夠聰明,無法在 InterlockedIncrement 之外進行正確的低鎖編程,但如果你是,那就去吧。 只要確保您在代碼中留下大量文檔,這樣那些不如您聰明的人就不會意外破壞您的內存模型不變量之一並引入一個無法找到的錯誤。

現在沒有“無鎖線程”這樣的東西。 對於學術界等來說,這是一個有趣的游樂場,早在上世紀末,當時計算機硬件又慢又貴。 Dekker 的算法一直是我最喜歡的,現代硬件已經把它放牧了。 它不再起作用了。

兩個發展已經結束了這種情況:RAM 和 CPU 速度之間的差距越來越大。 以及芯片制造商在一個芯片上放置多個 CPU 內核的能力。

RAM 速度問題要求芯片設計者在 CPU 芯片上放置一個緩沖區。 緩沖區存儲代碼和數據,可由 CPU 內核快速訪問。 並且可以以更慢的速度從/向RAM讀取和寫入。 這個緩沖區稱為 CPU 緩存,大多數 CPU 至少有兩個。 一級緩存小而快,二級緩存大而慢。 只要CPU可以從一級緩存中讀取數據和指令,它就會運行得很快。 緩存未命中非常昂貴,如果數據不在第一個緩存中,它會使 CPU 休眠多達 10 個周期,如果數據不在第二個緩存中,則多達 200 個周期並且需要從中讀取內存。

每個 CPU 內核都有自己的緩存,它們存儲自己的 RAM“視圖”。 當 CPU 寫入數據時,將寫入緩存,然后緩慢刷新到 RAM。 不可避免的是,每個內核現在對 RAM 內容都有不同的看法。 換句話說,一個 CPU 不知道另一個 CPU 寫了什么,直到 RAM 寫周期完成並且CPU 刷新自己的視圖。

這與線程非常不兼容。 當您必須讀取另一個線程寫入的數據時,您總是非常關心另一個線程的狀態。 為了確保這一點,您需要顯式編程一個所謂的內存屏障。 它是一種低級 CPU 原語,可確保所有 CPU 緩存都處於一致狀態並具有最新的 RAM 視圖。 所有掛起的寫入都必須刷新到 RAM,然后需要刷新緩存。

這在 .NET 中可用,Thread.MemoryBarrier() 方法實現了一個。 鑒於這是 lock 語句完成的 90% 的工作(以及 95% 以上的執行時間),避免使用 .NET 提供的工具並嘗試實現自己的工具,您根本就沒有領先。

谷歌用於無鎖數據結構軟件事務內存

我同意約翰斯基特的觀點; 無鎖線程是魔鬼的游樂場,最好留給知道自己需要知道什么的人。

盡管在 .NET 中實現無鎖線程可能很困難,但通常您可以通過研究需要鎖定的內容並最小化鎖定部分來在使用鎖時做出重大改進……這也稱為最小化鎖粒度

例如,只需說您需要使集合線程安全。 如果迭代集合的方法對每個項目執行一些 CPU 密集型任務,請不要盲目地鎖定它。 可能只需要鎖定創建集合的淺表副本。 迭代副本然后可以在沒有鎖的情況下工作。 當然,這在很大程度上取決於您的代碼的具體情況,但我已經能夠使用這種方法修復鎖車隊問題。

當涉及到多線程時,您必須確切地知道自己在做什么。 我的意思是探索在多線程環境中工作時可能發生的所有可能的場景/案例。 無鎖多線程不是我們合並的庫或類,它是我們在線程之旅中獲得的知識/經驗。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM