contributed by < ccs100203
>
linux2021q3
程式以 lock-free 以及 hazard pointer 的手法,建立相同量的 thread 做 insert / delete 的動作,上圖為 hazard pointer 的結構示意。
Hazard pointer 中使用到的 retire list 的結構
Hazard pointer 的主要結構
alignas(128) atomic_uintptr_t *hp[HP_MAX_THREADS];
hp
的實作是用一個類似二維陣列去儲存整個 hazard pointer 的內容
用 HP_MAX_THREADS
去決定 row 有多少,是因為每條 Thread 都要有自己的 hazard pointer,這部份可以從示意圖中看出來,詳細則在 Lock-Free Data Structures with Hazard Pointers 中描述。
alignas 定義在 C++ 11 中,可在宣告時用來對齊
the declaration or definition of a class/struct/union or enumeration;
the declaration of a non-bitfield class data member;
the declaration of a variable, except that it cannot be applied to the following:
a function parameter;
the exception parameter of a catch clause.
對齊可以避免 false sharing 以及 cacheline coherence 所造成的影響,retirelist 中使用的 CLPAD
就是來協助 cacheline padding 的。
為何是對齊 128 尚未釐清
max_hps
決定了每條 thread 中的 hazard pointer 有多少容量 (實作中貌似未檢查)
alignas(128) retirelist_t *rl[HP_MAX_THREADS * CLPAD];
每個 Thread 會有 CLPAD
個 retire list
此程式的結構都包裝在 list_t
之內,以其為 list 的主體。
magic
協助驗證正確性,確保該 node 的記憶體範圍並未被其他部份修改到。key
是 list 與 hazard pointer 中用來索引的 index,會以 tid 為依據。程式利用 tid,以遞增的方式對每條 thread 進行編號,存放在具有 thread_local 性質的 tid_v
中,代表該變數在每個 thread 具有獨立性,並有 thread storage duration。
atomic_int_fast32_t
這個 type 在 std::atomic 及 Fixed width integer types 之中有解釋,在 C++ 11 後有分成 int8_t
, int_fast8_t
, int_least8_t
。
int8_t
剛好 8 bit 的 intint_fast8_t
進行操作最快的 int,但是至少 8 bitint_least8_t
該 int 使用最少的 bit,但是至少 8 bit__list_find
是此程式的核心,這裡會以 lock-free 的手法,並讓 list 維持 ascending,以及讓某些條件下的 node 放入 retire list。回傳 true 代表找到該 key
在 list 內,並藉由 pointer to pointer 的方式回傳給 caller。
prev
的作用像是存放 curr
的複本,用來對照當前的 curr
是否已經被更改,是以 pointer to pointer 的方式。
try_again:
的作用是當程式發現當前操作的指標有被其他 thread 更動過時,就會重做
list_hp_protect_ptr
會將操作的節點放入 hazard pointer 中,代表當前的 thread 正在使用,作為避免被刪除的保護
第 14~17 行,確認此時的 prev
與 curr
是否相同,不同就要重做
2021/08/20
可以確認 head
是否被 mark
while 會對 list 進行尋訪,並找到適當的插入位置 (由小到大),在找到 curr->key >= key
或 tail 之前不會讓迴圈結束。而如果中途有找到 marked node,則會將該 node 放入 retire list 內,不作為回傳的結果使用。
第 21 行會將 next
也放入 hazard pointer 之中。再來會確認 local 的 next
是否與 list 中的 next
相同,因為有可能已經被插入新的節點了,也確認 prev
是否還等於 curr
。
第 35 行 get_unmarked_node(next) == next
是利用到 A Pragmatic Implementation of Non-Blocking Linked-Lists 的手法,被標記的 memory 區塊代表他是已經準備被刪除的了,所以就不會將此 next
作為要使用的下一個 node。
next
,然後用 list_hp_retire
將他放入 retire list裡,並進行處理。(get_unmarked_node(curr)->key < *key)
確認是否符合條件,要注意第 40 行的判斷,會回傳 true 讓程式不插入相同 key
值的節點。使用 list_hp_protect_release
將需要保護的 node 放入 hazard pointer 之中,利用 release 的 memory order 確保在這之前的 store 都以寫入。
第 6 行會先利用 list_node_new
根據傳入的 key
創一個新的 node,藉由 __list_find
得到插入的位置,要注意回傳 true 則代表有重複節點,所以把現在新創立的 destroy 並從 hazard pointer 中移除。
再來程式會將新的 node
的 next
先指向 curr
再來先透過 CAS 確保 prev
的位置還未被換過,然後將 node
放入原本 curr
的位置中
利用 aligned_alloc 確保配置的空間對齊 128。
使用 atomic_fetch_add 操作 inserts
,紀錄插入的次數。
用 __list_find
查找要刪除的節點是否在 list 中,沒有的話記得先從 hazard pointer 將當前保護的 memory 移掉再返回。
第 13 行的 CAS 不僅確保 next
未被更動,將 next
mark 可以確保不會被其他 thread 使用。
第 19 行的 CAS 先是確保 prev
與 curr
尚未被動過,然後將 next
unmark 並放入原本 list 內 curr
的位置,藉此將 curr
從 list 中移除。移除後的 curr
就交由 list_hp_retire
去處理。
在 Lock-Free Data Structures with Hazard Pointers 中有提及 retire list 的原理,當 list 內的東西達到一定量時,在這邊是利用 HP_THRESHOLD_R
,會去查找所有 hazard pointer 的內容,確認要刪除的 memory 不在 hazard pointer 的保護中,來判斷 retire list 內的記憶體是否可以釋放掉。
將傳入的 ptr
放入該 thread 對應的 retire list 中。
再藉由 HP_THRESHOLD_R
去判斷是否要開始進行回收的動作,因為程式的 HP_THRESHOLD_R
設為 0,所以每次有節點放入就會立刻進行查找的動作。(或許可以參照論文的建議將其調大)
第 9 行,會以每一個 retire list 內的節點,去 hazard pointer 中進行查找,如果有找到就會將 can_delete
設為 false,代表還不能刪除。
最後如果可以刪除,會利用 memmove 將 iret
之後記憶體空間往前移,覆蓋掉被刪除的位置,藉以達到從 retire list 中移除的效果,同時可以維持該陣列的結構。並將要刪除的節點傳入 deletefunc
刪除。(在程式中是 __list_node_delete
)
free
之前,會先對照 LIST_MAGIC
是否相同,藉由比對 magic number 來判斷該記憶體位置沒有被不正確的更改,保證程式的正確性。
同樣利用 atomic_fetch_add 操作 deletes
,紀錄刪除的次數。
TODO