无锁数据结构一
lock-free
本文對lock-free進行介紹,主要介紹原子性操作,下一篇介紹內存訪問控制
什么是lock-free
無鎖數據結構
無鎖數據結構的實現主要基于兩個方面:原子性操作和內存訪問控制方法。下面先記錄原子性操作相關知識。
原子性操作
原子性操作可以簡單地分為讀寫(read and write)、原子性交換操作(read-modify-write,RMW)兩部分。原子操作可認為是一個不可分的操作;要么發生,要么沒發生,我們看不到任何執行的中間過程,不存在部分結果(partial effects),就像事務。
在現代處理器中,簡單的數據類型的對齊讀寫操作一般是原子的,而RMW更進一步的,允許進行更復雜的原子事務性操作。
下面以x86架構為例介紹其如何實現原子性操作,其通過三種機制保證原子性:(詳情可參考1、2)
在不同平臺RMW操作有不同實現,如:
- _InterlockedIncrement?_InterlockedCompareExchange(Win32);
- OSAtomicAdd32(iOS);
- std::atomic<>::fetch_add``std::atomic<>::compare_exchange_strong?(c++11) .
在構建無鎖數據結構時需要用到RMW操作,其包括:compare-and-swap (CAS)、fetch-and-add (FAA)、test-and-set (TAS) 等等。其中最基本的是CAS,其他操作可通過CAS實現。
CAS
CAS偽代碼如下:
bool CAS( int * pAddr, int nExpected, int nNew ) atomically {if ( *pAddr == nExpected ) {*pAddr = nNew ;return true ;}elsereturn false ; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
但在實際中往往會想要知道,失敗后,pAddr的當前值是多少,故可修改如下。
int CAS( int * pAddr, int nExpected, int nNew ) atomically {if ( *pAddr == nExpected ) {*pAddr = nNew ;return nExpected ;}elsereturn *pAddr }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
c++11中CAS為?
std::atomic::compare_exchange_weak,?
std::atomic::compare_exchange_strong.?
其如同用std::memcmp?原子地比較?*this?的對象表示和?expected?的對象表示,而若它們逐位相等,則以?desired?替換前者(進行讀修改寫操作)。否則,將?*this?中的實際值加載進?expected?(進行加載操作)。如同用?std::memcpy?進行復制。詳情可查看cppreference
ABA問題
ABA問題是所以基于CAS基本類型的無鎖容器的一個巨大災難.CAS算法實現的一個重要前提是需要取出內存中某時刻的數據,而在下一時刻比較并替換,那么就存在了一個時間差,這個時間差會內數據有可能變化。
考慮一個棧數據順序為 ABC,線程1pop,執行到取出A值,但還沒CAS,此時要替換成的數據為A->next即B線程2pop了A、B,然后push?A?
,順序變為AC,然后線程1繼續執行CAS,比較時棧頂仍為A比較成功,替換成B(已不在棧中),棧數據出錯。
其解決方法有:標簽指針(Tagged pointers)、險象指針(Hazard pointer)等。在其他文章介紹。
對于實現了load-linked、store-conditional (LL/SC) 這樣的操作對的處理器,其不會發生ABA問題。
其偽代碼如下:
word LL( word * pAddr ) { return *pAddr ; }bool SC( word * pAddr, word New ) {if ( data in pAddr has not been changed since the LL call) {*pAddr = New ; return true ;}else return false ; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
LL/SC對以括號運算符的形式運行,Load-linked(LL) 運算僅僅返回 pAddr 地址的當前變量值。如果 pAddr 中的數據在讀取之后沒有變化,那么 Store-conditional(SC) )操作會將LL讀取 pAddr 地址的數據存儲起來。這種變化之下,任何 pAddr 引用的緩存行修改都是明確無誤的。為了實現 LL/SC 對,程序員不得不更改緩存結構。簡而言之,每個緩存行必須含有額外的比特狀態值(status bit)。一旦LL執行讀運算,就會關聯此比特值。任何的緩存行一旦有寫入,此比特值就會被重置;在存儲之前,SC操作會檢查此比特值是否針對特定的緩存行。如果比特值為1,意味著緩存行沒有任何改變,pAddr 地址中的值會變更為新值,SC操作成功。否則本操作就會失敗,pAddr 地址中的值不會變更為新值。
CAS通過LL/SC對得以實現,具體如下:
bool CAS( word * pAddr, word nExpected, word nNew ) {if ( LL( pAddr ) == nExpected ) return SC( pAddr, nNew ) ; return false ; }- 1
- 2
- 3
- 4
- 5
但LL/SC由于偽共享(False sharing)也存在問題,簡單來說即是多個共享變量使用同一緩存行,只要有一個變量改變,即認為緩存行數據無效(SC失敗),詳情可參考4。
NOTE
C++11的原子標準并不保證其在所以平臺的實現都是lock-free的(std::atomic_flag 除外),可通過std::atomic<>::is_lock_free確認。
All atomic types except for std::atomic_flag may be implemented using mutexes or other locking operations, rather than using the lock-free atomic CPU instructions. Atomic types are also allowed to be sometimes lock-free, e.g. if only aligned memory accesses are naturally atomic on a given architecture, misaligned objects of the same type have to use locks.
參考資料
總結
- 上一篇: 互联网广告系统综述四定向
- 下一篇: 无锁数据结构二-乱序控制(栅栏)