contributed by < kevinshieh0225
>
2
:Hazard Pointer以下說明參考測驗
2
之說明,並補充前備知識
在並行程式設計中,當我們在存取共用的記憶體物件時,需要考慮到其他執行緒是否有可能也正在存取同一個物件,若要釋放該記憶體物件時,不考慮這個問題,會引發嚴重的後果,例如 dangling pointer。
Dangling pointers and wild pointers in computer programming are pointers that do not point to a valid object of the appropriate type. These are special cases of memory safety violations.
使用 mutex 是最簡單且直觀的方法:存取共用記憶體時,acquire lock 即可保證沒有其他執行緒正在存取同一物件,也就可安全地釋放記憶體。但若我們正在存取的是一種 lock-free 資料結構,當然就不能恣意地使用 lock,因為會違反 lock-free 特性,即無論任何執行緒失敗,其他執行緒都能可繼續執行。於是乎,我們需要某種同為 lock-free 的記憶體物件回收機制。
lock-free: 強調以系統的觀點來看,只要執行足夠長的時間,至少會有一個執行緒會有進展 (progress)
Compare-and-swap
對於 C 這樣缺乏內建 concurrent GC 機制的程式語言來說,若要實作 lock-free 演算法,就要自行處理記憶體釋放的議題。
ABA 問題:執行緒
1
希望 pop(A -> B -> C)的佇列變成(B -> C),執行了compare_and_swap(target=&head, newvalue=B, expected=A)
。但是如果有另一個執行緒2
他已經執行兩次 pop 並且又把 A push 回去、把 B 釋放了(A -> C),那執行緒1
會誤以為佇列在我操作中間沒有被操作,而錯誤把空指標B
指派為新的首節點。Problem without automatic garbage collection:在進行指標指派中途如果有其他執行緒將指標釋放了,那可能導致執行上的非預期結果。
Hazard pointer 是其中一種解決方案,其原理是讀取端執行緒對指標進行識別,指標 (特別是指向的記憶體區塊) 若要釋放時,會事先保存,延遲到確認沒有讀取端執行緒,才進行真正的釋放。
Lock-Free Data Structures with Hazard Pointers
Each reader thread owns a single-writer/multi-reader shared pointer called "hazard pointer." When a reader thread assigns the address of a map to its hazard pointer, it is basically announcing to other threads (writers), "I am reading this map. You can replace it if you want, but don't change its contents and certainly keep your deleteing hands off it."
在 hazard pointer 架構中,每個 thread 首先需要各自維護的兩個關鍵集合是:
要安全的釋放記憶體,其基本想法就是:
測驗題的程式碼已有更新,可見 hp_list,注意仍可改進,使用 relaxed memory model
hptr.h
提供一個 lock-free 的 linked list 操作的結構體與介面,此介面同時提供給 hazard pointer list
和 retired list
使用。
特別說明 list_insert_or_append
和 list_remove
:
list_insert_or_append
尋訪 list 節點,利用 atomic_load
將該節點值載入,如果該節點為 0 ,並且利用 atomic_cas
確認並未被其他執行緒競爭以插入新節點,如果尋訪後都沒有成功,再進行 list_append
。
list_remove
尋訪 list 節點確認是否有我們要刪除的目標,如果有目標節點的話,利用atomic_cas
確認並未被其他執行緒先刪除,以更換 nullptr。
透過 domain.h
對 hp list
和 retire list
進行操作。分為以下指令:
幾個比較容易混淆的函式解釋:
load
將資源加入 hplist
,並透過 drop
移出 hplist
。這個 hplist 並未限定一個資源只能載入一次,所以說幾個 thread 在使用這份資源,hplist
中就會有幾個此資源的 hp
。所以所有 load
進 hplist
來的資源都應可執行對應個 drop
。而當 hplist
不再記載此資源時,便可以將此資源刪除。cleanup_ptr
指定特定資源刪除,搭配 swap
使用;而 cleanup
是要刪除 retired list 上的所有資源。swap
使用新資源取代舊資源,並利用 cleanup_ptr
刪除該資源。利用 swap
提供 writer_thread
更新共享資源的方式,帶來一個好處:
因為我們是先做新舊資源交換,在讓舊資源等待釋放,所以克服了 writer 需要 等待 reader 完成的 starving。
config_t
是本次實驗使用的共享資源,利用 create_config
初始化共享資源、delete_config
刪除、print_config
顯示共享地址資訊。
其中我們看到 init
:
shared_config
是共享資源物件,config_dom
是 hplist
,這點出幾個重點:
domain_t config_dom
也就是說在這個實作中沒有各自的 retired list
,hplist
和 retired list
是大家一同管理。cleanup
,行程結束後就一起 deallocate 了。參考 bakudr18
使用了 nanosleep 讓每次 read 成功後暫停 thread 執行時間 150 ns 、 write 成功後暫停 50 ns,讓 reader 與 writer 比較容易能交互執行。
先試試 multiple reader = 3 沒問題,但 multiple writer = 3 出現 data race 和 heap-use-after-free 的警告。
我嘗試將 thread PID 和使用的記憶體位置 printf
出來是符合輸出預期的,依據觀察 data race 的執行區域發生在兩個以上的 writer_thread
第一次輸出與第二次輸出錯開的時候:
當我把 writer_thread
第二次輸出註解時,data race 便被解除。
依據網路上相關的線索說明,printf
的成本代價很高(須使用 system call,並且用 mutex lock 來確保單一執行緒執行),故使用 printf
在多執行緒下將改變執行環境,更好的方式是將要輸出的結果先存在一個資料空間,最後再一同把執行結果輸出。這正是老師的 Concurrent Linked List With Hazard Pointer 教材中使用的方法。
在閱讀 Lock-Free Data Structures with Hazard Pointers 後有幾個認為此實作能改進之處:
本次實作的 hplist 和 retired 是所有執行緒共用的,這和原本 HPlist 的設計有些不同,原本的 HPlist 只會提供一個執行緒一個 hplist* head
,每個 thread 各自管理自己的 retired list
。這使的程式設計的情境不同。因為在本次實作中的 write thread 是直接將資源換新,隨即把舊資源刪除,不會發生兩個 thread 同時要釋放同一份資源的情形;但今天如果 retired list 各自維護,那就有可能兩個行程序希望同時釋放同一筆資源的衝突。
Nothing up our sleeves! Now, the
Scan
function will perform a set difference between the point the current thread’s retired list, and all hazard pointers for all threads. What does that set difference mean? Let’s think of it for a second: it’s the set of all old pMap_ pointers that this thread considers useless, except those that are found among the hazard pointers of all threads. But, hey, these are exactly the goners! By definition of the retired list and that of the hazard pointers, if a pointer is retired and not marked as “hazardous” (i.e. “in use”) by any thread, the set intersection of the two sets yields precisely the pointers that can bedeleted
另外論文提及能夠使用 hashmap 的方式設計 hazard pointer list,讓 delete
在搜尋節點為常數時間,而讓完整的 retired list 內容刪除理想上只需要 的時間複雜度。
在 quiz6 的第二題老師實作了一份 lock-free hashmap,可以納入 hazard pointer 的實作中。
討論本實作中的一些手段與技巧,另外探討 memory ordering:在本次實作中的 atomic instruction 的 ordering 限制都是用高度最強的 __ATOMIC_SEQ_CST
,然而並不是所有情況都有需要用這麼強的限制。
6.55 Built-in Functions for Memory Model Aware Atomic Operations
std::memory_order
管理的資源為 list_node_t
,list_t
是 list_node_t
的資源串列,並也包含著 list_hp_t
,所有的 thread 都從 list_t
讀寫資源。
list_hp_t
是本實作的 hp_list 和 retire_list 結構體。其中 hp_list 增加了 tid() 的執行緒索引,執行緒可以根據被分配到的索引去管理並寫入 hp_list index。
為了避免 printf 造成的執行成本,此實作將操作次數存入 struct runtime_statistics
中,最後再把紀錄結果一次輸出。
使用 atomic_fetch_add
讓執行緒們共同紀錄一份共享結構體。
atomic_fetch_add
的巨集展開如下:
#define atomic_fetch_add(x, a) __atomic_fetch_add(x, a, __ATOMIC_SEQ_CST)
在這個案例中應不需要用到 __ATOMIC_SEQ_CST
的強度。只要確保 atomic 了,執行的先後順序並不影響輸出結果,用 __ATOMIC_RELAXED
就好
這次實作的 hp_list
透過 alignas(128)
強制使的 hp
, rt
需要對齊 128 排列,這些串列頻繁在執行緒間讀寫,為了避免 cacheline 重疊的抓取了兩筆獨立的資料,而發生 false sharing 的情形:
為了對齊,結構體的操作也需要改變,以 list_hp_new
為例:
實作中使用 aligned_alloc(128, sizeof(*hp))
取代原本的 alloc
。
還在研究 CLPAD 是什麼。
list_hp_retire
如果為了補刪除的缺向前補齊,iret
索引值是不是也要相應減一?否則原本 rl->listp[iret+1]
的物件不會被檢查到。
pull request 紀錄小抄
git rebase
Git 刪除已 Push 至遠端分支的 Commit 教學與範例
How to Write a Git Commit Message
list_insert
, list_delete
, __list_find
關於 get_marked
的巨集的功能應該如同論文中提到的:在刪除前必須對刪除資料做標記,否則如果兩筆資料同時要刪除,會產生衝突。
Nothing up our sleeves! Now, the
Scan
function will perform a set difference between the point the current thread’s retired list, and all hazard pointers for all threads. What does that set difference mean? Let’s think of it for a second: it’s the set of all old pMap_ pointers that this thread considers useless, except those that are found among the hazard pointers of all threads. But, hey, these are exactly the goners! By definition of the retired list and that of the hazard pointers, if a pointer is retired and not marked as “hazardous” (i.e. “in use”) by any thread, the set intersection of the two sets yields precisely the pointers that can bedeleted