無鎖數據結構(四)

 

無鎖數據結構(四)

Andrei Alexandrescu

December 16, 2007

譯者:張桂權

12/25/2007


初稿階段,沒有得到許可不得引用,否則後果自負

6 寫鎖(Write-LockedWRRM 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是如何工作的呢我們使用到現在很熟悉的變量oldfresh。但是現在的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日
----------------------------------------
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章