無鎖數據結構(四)
Andrei Alexandrescu
December 16, 2007
譯者:張桂權
12/25/2007
(初稿階段,沒有得到許可不得引用,否則後果自負)
6 寫鎖(Write-Locked)WRRM Maps
爲了瞭解敵人的邪惡,首先嚐試一個經典的引用計數實現,並弄清失敗的原因是非常具有教育意義的。所以,讓我們思考一個使用map指針的引用計數,用WRRMMap存儲一個指向這種形式的機構的指針。
template <class K, class V>
class WRRMMap {
typedef std::pair<Map<K, V>*,
unsigned> Data;
Data* pData_;
...
};
美極了。現在,Lookup對pData_->second加1,然後查找map中所有它想要的項,最後把pData_->second減1。當引用計數值爲0時,pData_->first可以被delete了,然後pData_自身也被delete了。似乎連傻子都懂(foolproof),但是……
但是,這實在太“愚蠢(foolful)”了(或“連傻子都懂”的任意反義詞)。試想,正當某個線程發現引用計數值爲0,並且開始刪除pData_時,另外一個線程……不,更好點:一個bazillion線程已經鎖住垂死的pData_,並且準備讀取其中的數據項。無論你的方案有多靈巧,都將碰上這個基礎的catch-22(競爭):讀取指向數據的指針,一個需要增加引用計數;但是計數器必須是數據本身的一部分,所以沒有訪問指針之前它不能讀取數據。這有點像電子柵欄,在其頂端有一個開-關按鈕:爲了安全的攀越柵欄,你首先需要關掉它,但是關掉它的目的不是你需要攀越它。
所以,讓我們思考其他的方法來恰當的delete舊的map。一個解決方案是等待,然後delete。舊的pMap_對象在處理器運行一毫秒之內將會被少之又少的線程查找;這是因爲新的查找將使用新的map,一旦CAS之間活動的查找結束,pMap_已經準備好Hades(傾斜)了。因此,一個方案應該在一個循環中給某個“蟒蛇”(boa serpent)線程排列舊的pMap_的值。這個線程休眠,大約,200毫秒,然後喚醒,delete最近的map,進入消化休眠。
這不是一個理論上安全的方案(雖然,實際上在邊界之內很不錯)。不管出於什麼理由,最邪惡的一件事情是,如果一個查找線程被延遲太久了,“蟒蛇”線程將在這個線程的腳下delete這個map。可以通過一直給“蟒蛇”線程賦一個低於其它任何線程的優先級,但是作爲一個整體的方案,有了“臭氣”之後,就很難消除了。如果你也同意,用嚴肅的表情很難防禦這種技術,那就讓那個我們繼續吧。
另外一個方案[4],依賴於一個經過擴展的DCAS原子指令,它可以在內存中比較和交換兩個非共邊(non-contiguous)的字。
template <class T1, class T2>
bool DCAS(T1* p1, T2* p2,
T1 e1, T2 e2,
T1 v1, T2 v2) {
if (*p1 == e1 && *p2 == e2) {
*p1 = v1; *p2 = v2;
return true;
}
return false;
}
很自然了,這兩個位置是指針和引用技術本身。DCAS已經在Motorola 68040處理器上實現(非常的低效),而不是其它的處理器。因爲基於DCAS的方案被認爲只有理論價值。
第一發子彈目標是一個具有確定性解構的方案,依賴於很少需求的CAS2。前面曾經提到,很多32位機器實現了64位的CAS,通常被稱作CAS2。(因爲它僅操作共邊的字,很顯然CAS2沒有DCAS有效。)爲了啓動器,我們將引用計數的值保存到它看守的指針之後:
template <class K, class V>
class WRRMMap {
typedef std::pair<Map<K, V>*,
unsigned> Data;
Data data_;
...
};
(注意了,現在我們把計數保存到它保護的指針的後面,這樣我們就可以擺脫前面提到的catch-22問題了。我們將看它在啓動一分鐘內的代價。)
當訪問map之前,我們改變Lookup來給引用計數加1,之後進行減1。爲了簡明扼要,在下面的代碼段中,我們將忽略異常安全問題(謹慎使用標準計數可以實現):
V Lookup(const K& k) {
Data old;
Data fresh;
do {
old = data_;
fresh = old;
++fresh.second;
} while (CAS(&data_, old, fresh));
V temp = (*fresh.first)[k];
do {
old = data_;
fresh = old;
--fresh.second;
} while (CAS(&data_, old, fresh));
return temp;
}
最後,Update用一個新的來替換這個map——但是僅當引用計數爲1時有機會窗口的纔可以。
void Update(const K& k,
const V& v) {
Data old;
Data fresh;
old.second = 1;
fresh.first = 0;
fresh.second = 1;
Map<K, V>* last = 0;
do {
old.first = data_.first;
if (last != old.first) {
delete fresh.first;
fresh.first =new Map<K, V>(old.first);
fresh.first->insert(make_pair(k, v));
last = old.first;
}
} while (!CAS(&data_, old, fresh));
delete old.first; // whew(嚄, 哨聲)
}
這裏Update是如何工作的呢?我們使用到現在很熟悉的變量old和fresh。但是現在的old.second(計數)從來不會通過data_.second賦值;它一直都是1。這就意味着,Update將一直循環,直到它有一個用另外一個擁有計數器值爲1的指針來替換指向一個計數器的值爲1的指着的機會的窗口。用淺顯的英語來說,這個循環會說“我將用一個新的map替換這個舊的,更新了一個之後,我將開始禁戒其它任何一個更新這個map,但是當且僅當只有一個map存在的時候我才進行替換。”變量last和相關的代碼僅僅是一個優化:避免在舊的map還沒有替換完(僅這計數器),而反反覆覆的重新構建map。
夠整潔吧?差遠了。Update現在加鎖了:在有機會更新map之前,它需要等待所有的Lookup結束。隨風飄揚是無鎖數據結構最好性質。尤其是,很容易使Update餓死(starve Update to death):足夠高的頻率查找map——但是引用計數將永遠都不會降到1。所以到目前爲止我們得到的不是一個WRRM(Write-Rarely-Read-Many,少寫多讀)map,而是一個WRRMBNTM(Write-Rarely-Read-Many-But-Not-Many,少寫多讀,但不是太多)map。
---------------源文檔------------------
Lock-Free Data Structures
Andrei Alexandrescu
December 17, 2007
http://erdani.org/publications/cuj-2004-10.pdf
最後一次訪問時間:2007年12月30日
----------------------------------------