本章中的例子中,看到了一些復雜的代碼可讓無鎖結(jié)構(gòu)工作正常。如果要設計自己的數(shù)據(jù)結(jié)構(gòu),一些指導建議可以幫助你找到設計重點。第6章中關于并發(fā)通用指導建議還適用,不過這里需要更多的建議。我從例子中抽取了幾個實用的指導建議,在你設計無鎖結(jié)構(gòu)數(shù)據(jù)的時候就可以直接引用。
std::memory_order_seq_cst的原型std::memory_order_seq_cst比起其他內(nèi)存序要簡單的多,因為所有操作都將其作為總序。本章的所有例子,都是從std::memory_order_seq_cst開始,只有當基本操作正常工作的時候,才放寬內(nèi)存序的選擇。在這種情況下,使用其他內(nèi)存序就是進行優(yōu)化(早起可以不用這樣做)。通常,當你看整套代碼對數(shù)據(jù)結(jié)構(gòu)的操作后,才能決定是否要放寬該操作的內(nèi)存序選擇。所以,嘗試放寬選擇,可能會讓你輕松一些。在測試后的時候,工作的代碼可能會很復雜(不過,不能完全保證內(nèi)存序正確)。除非你有一個算法檢查器,可以系統(tǒng)的測試,線程能看到的所有可能性組合,這樣就能保證指定內(nèi)存序的正確性(這樣的測試的確存在),僅是執(zhí)行實現(xiàn)代碼是遠遠不夠的。
這里與無鎖代碼最大的區(qū)別就是內(nèi)存管理。當有其他線程對節(jié)點進行訪問的時候,節(jié)點無法被任一線程刪除;為避免過多的內(nèi)存使用,還是希望這個節(jié)點在能刪除的時候盡快刪除。本章中介紹了三種技術(shù)來保證內(nèi)存可以被安全的回收:
等待無線程對數(shù)據(jù)結(jié)構(gòu)進行訪問時,刪除所有等待刪除的對象。
使用風險指針來標識正在被線程訪問的對象。
在所有例子中,主要的想法都是使用一種方式去跟蹤指定對象上的線程訪問數(shù)量,當沒有現(xiàn)成對對象進行引用的時候,將對象刪除。當然,在無鎖數(shù)據(jù)結(jié)構(gòu)中,還有很多方式可以用來回收內(nèi)存。例如,理想情況下使用一個垃圾收集器。比起算法來說,其實現(xiàn)更容易一些。只需要讓回收器知道,當節(jié)點沒被引用的時候,回收節(jié)點,就可以了。
其他替代方案就是循環(huán)使用節(jié)點,只在數(shù)據(jù)結(jié)構(gòu)被銷毀的時候才將節(jié)點完全刪除。因為節(jié)點能被復用,那么就不會有非法的內(nèi)存,所以這就能避免未定義行為的發(fā)生。這種方式的缺點:產(chǎn)生“ABA問題”。
在“基于比較/交換”的算法中要格外小心“ABA問題”。其流程是:
本章提到的算法不存在這個問題,不過在無鎖的算法中,這個問題很常見。解決這個問題的一般方法是,讓變量x中包含一個ABA計數(shù)器?!氨容^/交換”會對加入計數(shù)器的x進行操作。每次的值都不一樣,計數(shù)隨之增長,所以在x還是原值的前提下,即使有線程對x進行修改,“比較/交換”還是會失敗。
“ABA問題”在使用釋放鏈表和循環(huán)使用節(jié)點的算法中很是普遍,而將節(jié)點返回給分配器,則不會引起這個問題。
在最終隊列的例子中,已經(jīng)見識到線程在執(zhí)行push操作時,必須等待另一個push操作流程的完成。等待線程就會被孤立,將會陷入到忙等待循環(huán)中,當線程嘗試失敗的時候,會繼續(xù)循環(huán),這樣就會浪費CPU的計算周期。當忙等待循環(huán)結(jié)束時,就像一個阻塞操作解除,和使用互斥鎖的行為一樣。通過對算法的修改,當之前的線程還沒有完成操作前,讓等待線程執(zhí)行未完成的步驟,就能讓忙等待的線程不再被阻塞。在隊列例中,需要將一個數(shù)據(jù)成員轉(zhuǎn)換為一個原子變量,而不是使用非原子變量和使用“比較/交換”操作來做這件事;要是在更加復雜的數(shù)據(jù)結(jié)構(gòu)中,這將需要更加多的變化來滿足需求。