# 2021q3 Homework2 (hp)
contributed by < `conc95xh` >
:::warning
注意共筆書寫規範: 中英文間用一個半形空白字元區隔
:notes: jserv
:::
## 相關閱讀作業
本次作業主要以 [Lock-Free Data Structures with Hazard Pointers](https://erdani.org/publications/cuj-2004-12.pdf) 這篇論文為基底,該論文只有闡述如何實現在 lock-free 的情況下更新且釋放一個可能正在被讀取的指標,本次作業則去實作一個 lock-free 的 linked list,包含
* 插入新節點
* 刪除節點
* 給定一個 key 搜尋相同key的節點 並傳回適當位置
* 若 linked list 中有完全相等的 key ,則傳回 curr 為指向該節點, prev 指向前一個節點的next field, next 則指向 curr 的下一個節點
* 若 linked list 中沒有完全相等的 key,則傳回的 curr 指向的節點為key 比傳入 key 還大的節點,且 prev 指向前一個節點的 next field,且其 key 比傳入的 key 還小
對於插入新節點的操作,我們預期若 linked-list 裡沒有跟新節點一樣的 key,我們可以先把新節點的 next 指向 curr ,再把 *prev 改寫成新節點的位置(atomic operation)
對於刪除一個節點,我們預期若 linked-list find 找到一個key完全相同的節點,我們可以直接把 *prev 改寫成指向 next,即完成刪除(atomic operation)
不管插入或刪除,都是在對 linked-list 的操作為一個 atomic operation 的前提之下,因此必須使用 CAS 操作(check if the field's value is the same as the expected value, then replace the field's value with the desired value)
儘管以上前提都符合了,仍然有一個問題:
```
若被刪除的節點仍然有執行緒在讀取使用,則記憶體何時該釋放?
```
在論文中,提出一個方法:即 Hazard pointer,任意一個執行緒在取用一個節點時,必須將其加入該 thread 的 Hazard pointer list 裡,以向其他執行緒宣告此節點被讀取中。
若有任何刪除節點的行為,不需要鎖定防止其他執行緒干擾,只要刪除的操作命令為 atomic operation 即可從 linked-list 移除,但必項保證該刪除節點的記憶體區塊不會被釋放,直到該節點不在任何 thread 的 hazard pointer list中(即retire)
Hazard pointer 的靈魂即為讀取的執行緒要讀取一個節點的內容時,先
* 把取得節點的位置放入 Hazard pointer list 裡
* 再確認一下取得節點的狀態是不是在放入 Hazard pointer 前的狀態一樣
* 若為真,則該節點可安心讀取不必當心被釋放
* 若為假,則重新操作或放棄(如第10行的 try_again)
因此可以發現,在以下程式碼第13行我們將取得的 curr 放入 hazard pointer 時,隨即檢查 `*prev == unmarked curr` 我們在12行時藉由prev取得 curr,因此 prev 不是`&list->head` 就是上一個一樣放入 hazard pointer 的節點的 next field,因此可以放心讀取不怕存取到被釋放的記憶體
```c=
static bool __list_find(list_t *list,
list_key_t *key,
atomic_uintptr_t **par_prev,
list_node_t **par_curr,
list_node_t **par_next)
{
atomic_uintptr_t *prev = NULL;
list_node_t *curr = NULL, *next = NULL;
try_again:
prev = &list->head; //prev為指向指向curr的指標,因此為**
curr = (list_node_t *) atomic_load(prev);
(void) list_hp_protect_ptr(list->hp, HP_CURR, (uintptr_t) curr);
if (atomic_load(prev) != get_unmarked(curr))
goto try_again; //將curr放入hp時,仍須再檢查一次*prev
//指向curr,有可能在放入前被插斷,且被更
//改,若結果為真,則代表放入hp成功
while (true) {
if (!get_unmarked_node(curr))
return false;
next = (list_node_t *) atomic_load(&get_unmarked_node(curr)->next);
(void) list_hp_protect_ptr(list->hp, HP_NEXT, get_unmarked(next));
if (atomic_load(&get_unmarked_node(curr)->next) != (uintptr_t) next)
break; //21和23行這兩段break無法理解,
//break後,prev跟curr中間不一定是最適當
//插入的位置,因為list有可能再
//也不是ordered list?
if (get_unmarked(next) == atomic_load((atomic_uintptr_t *) &list->tail))
break;
if (atomic_load(prev) != get_unmarked(curr))
goto try_again;
if (get_unmarked_node(next) == next) {
if (!(get_unmarked_node(curr)->key < *key)) {
*par_curr = curr;
*par_prev = prev;
*par_next = next;
return *key == get_unmarked_node(curr)->key; //Q:FFF
}
prev = &get_unmarked_node(curr)->next;
(void) list_hp_protect_release(list->hp, HP_PREV,
get_unmarked(curr));
} else { //若curr已經被mark成將要刪除了,則將這個節點
// 刪除,刪除成功則繼續往下找
// 失敗則重來,因此代表 curr 的 next 不一定有意義
uintptr_t tmp = get_unmarked(curr);
if (!atomic_compare_exchange_strong(prev, &tmp, get_unmarked(next)))
goto try_again;
list_hp_retire(list->hp, get_unmarked(curr));
}
curr = next;
(void) list_hp_protect_release(list->hp, HP_CURR, get_unmarked(next));
}
*par_curr = curr;
*par_prev = prev;
*par_next = next;
return false;
}
```
## 插入和刪除
不管插入和刪除,利用上述 `__list_find` 得到 prev, curr, next (即節點的相鄰資訊) 後,我們仍然需要利用 CAS 去確認一些事情,並且確認和執行為不可分割的操作
譬如插入節點,我們先確認 *prev==curr,若為真我們才可以把新的node插在兩者中間,若為假,則傳回的 prev,curr,next 都沒有意義,所有動作得重新再來,因此我們可以發現,所有動作都在一個 while (true) 的迴圈中
```c=
bool list_insert(list_t *list, list_key_t key)
{
list_node_t *curr = NULL, *next = NULL;
atomic_uintptr_t *prev = NULL;
list_node_t *node = list_node_new(key);
while (true) {
if (__list_find(list, &key, &prev, &curr, &next)) {
list_node_destroy(node);
list_hp_clear(list->hp);
return false;
}
atomic_store_explicit(&node->next, (uintptr_t) curr,
memory_order_relaxed);
uintptr_t tmp = get_unmarked(curr);
if (atomic_compare_exchange_strong(prev, &tmp, (uintptr_t) node)) {
list_hp_clear(list->hp);
return true;
}
}
}
```
刪除節點則為確認 curr 的 next field 沒有被mark,如果沒有則 mark 成delete,因為是 atomic operation,所以沒有其他人可以同時把它 mark 成delete
若成功則保證只有我們將 curr 標記為 deleted,
```
若下一個節點同時被 mark 成 deleted 且被移除了怎麼辦?
這樣我們不就把前一個節點的 next 指到一個即將被釋放的node?
```
next 的 next field 也有可能被mark,但它在會在第18行檢查 *prev 是否為 unmarked curr 時,失敗,直接回傳true,下次再由 __list_find() 的第39行執行真正的移除動作,因此我們不必擔心我們的 prev 會指向一個也已被移除的節點
```c=
bool list_delete(list_t *list, list_key_t key)
{
list_node_t *curr, *next;
atomic_uintptr_t *prev;
while (true) {
if (!__list_find(list, &key, &prev, &curr, &next)) {
list_hp_clear(list->hp);
return false;
}
uintptr_t tmp = get_unmarked(next);
if (!atomic_compare_exchange_strong(&curr->next, &tmp,
get_marked(next)))
continue;
tmp = get_unmarked(curr);
if (atomic_compare_exchange_strong(prev, &tmp, get_unmarked(next))) {
list_hp_clear(list->hp);
list_hp_retire(list->hp, tmp); //Q:DDD
} else {
list_hp_clear(list->hp);
}
return true;
}
}
```
## 釋放記憶體
list_hp_retire 即為釋放記憶體的主角,傳入一個 ptr,它會先將 PTR 加入retire list,且先檢查每一個在 retire list的ptr是否也在 Hazard pointer 列表中,若無則執行清除釋放動作,因此不只清除自己傳入的 ptr,也會順手把之前積存的 retired pointer 清除
```c=
void list_hp_retire(list_hp_t *hp, uintptr_t ptr)
{
retirelist_t *rl = hp->rl[tid() * CLPAD];
rl->list[rl->size++] = ptr;
assert(rl->size < HP_MAX_RETIRED);
if (rl->size < HP_THRESHOLD_R)
return;
for (size_t iret = 0; iret < rl->size; iret++) {
uintptr_t obj = rl->list[iret];
bool can_delete = true;
for (int itid = 0; itid < HP_MAX_THREADS && can_delete; itid++) {
for (int ihp = hp->max_hps - 1; ihp >= 0; ihp--) {
if (atomic_load(&hp->hp[itid][ihp]) == obj) {
can_delete = false;
break;
}
}
}
if (can_delete) {
size_t bytes = (rl->size - iret) * sizeof(rl->list[0]);
memmove(&rl->list[iret], &rl->list[iret + 1], bytes);
rl->size--;
hp->deletefunc((void *)obj); ///Q:RRR
}
}
}
```
## 改善
移除 __list_find 第21及第23行兩個compound statements,因為若在該點break,可能會發生ordered list無法順利排序(?)
嘗試將此程式修改成如下,執行結果相同
```diff=
diff --git a/list/list.c b/list/list.c
index 5bb75c0..b3347d7 100644
--- a/list/list.c
+++ b/list/list.c
@@ -231,13 +231,19 @@ try_again:
goto try_again;
while (true) {
if (!get_unmarked_node(curr))
- return false;
+ break;
next = (list_node_t *) atomic_load(&get_unmarked_node(curr)->next);
(void) list_hp_protect_ptr(list->hp, HP_NEXT, get_unmarked(next));
- if (atomic_load(&get_unmarked_node(curr)->next) != (uintptr_t) next)
- break;
- if (get_unmarked(next) == atomic_load((atomic_uintptr_t *) &list->tail))
- break;
+#if 0
+ if (atomic_load(&get_unmarked_node(curr)->next) != (uintptr_t) next) {
+ printf("%s:%s:%d\n", __file__, __fun__, __LINE__);
+ break;
+ }
+ if (get_unmarked(next) == atomic_load((atomic_uintptr_t *) &list->tail)) {
+ printf("%s:%s:%d\n", __file__, __fun__, __LINE__);
+ break;
+ }
+#endif
if (atomic_load(prev) != get_unmarked(curr))
goto try_again;
if (get_unmarked_node(next) == next) {
@@ -259,10 +265,12 @@ try_again:
curr = next;
(void) list_hp_protect_release(list->hp, HP_CURR, get_unmarked(next));
}
+#if 0
*par_curr = curr;
*par_prev = prev;
*par_next = next;
+#endif
return false;
}
```
TODO:
- [x] 執行結果是否一樣?
- [ ] 執行速度跟原先程式比較?
## 對比 rcu_list,解釋同為 lock-free 演算法,跟上述 Hazard pointer 手法有何異同?能否指出 rcu_list 實作缺陷並改進?
在老師提供的範例模板中,針對所有的 rcu_read_lock ,會將現在所有 read threads 串接到 zombie 串列中,並在 rcu_read_unlock 時,檢查自己是否為最後一個 reader ,若為最後一個,則執行清除 zombie list 及 free zombie(dead node) 的動作,若否,則留待 (defer),直到下一個 reader 退出 reader section 且發現自己是 last reader,幫忙處理 free 的動作。
因此獲得一個好處,即所有的 reader 在標記 reader critical section 時,是 lock-free 的,不用等待其他任何 thread 釋放 lock (包含 writer thread )
競爭的部分只有在將自己串接到 zombie list 的開頭時,需要用 atomic compare & swap 去確認剛才拿到的 old head 是否還有效,如下程式碼,只有在這裡才會付出等待的代價
```c=
void rcu_read_lock(read_handle_t *handle)
{
zombie_node_t *z_node = make_zombie_node();
z_node->owner = handle;
handle->zombie = z_node;
rcu_list_t *list = handle->list;
zombie_node_t *old_head;
__atomic_load(&list->zombie_head, &old_head, __ATOMIC_SEQ_CST);
do {
__atomic_store(&z_node->next, &old_head, __ATOMIC_SEQ_CST);
} while (!__atomic_compare_exchange(&list->zombie_head, &old_head, &z_node,
true, __ATOMIC_SEQ_CST,
__ATOMIC_SEQ_CST));
}
```
在 rcu_read_unlock 時,執行緒會檢查自己後面的其他 readers 是否已完成,若已完成,我們可以代為執行 free的動作。
如下 free(dead_node) 即為將之前已移除但還有 reader 在使用的 node 釋放掉,因為若 last 為 true ,則代表在我們之前的所有 readers 已跳出 reader critical section (注意:我們後面可能還有 readers,但我們不在意,我們只關注我們之前有沒有 readers )
```clike=
if (last) {
while (n) {
list_node_t *dead_node = n->zombie;
free(dead_node);
zombie_node_t *old_node = n;
__atomic_load(&n->next, &n, __ATOMIC_SEQ_CST);
free(old_node);
}
__atomic_store(&z_node->next, &n, __ATOMIC_SEQ_CST);
}
void *null = NULL;
__atomic_store(&z_node->owner, &null, __ATOMIC_SEQ_CST);
```
(條件a):「我們之前所有的 readers 已跳出 read-side critical section」
修件a 這句話若為真有什麼意義呢? 我想若在此時把我們自己也塞入 zombie-list ,是不是代表我們對 rcu-list 某個節點做的移除更動,在條件a成立後,沒有人再需要被移除的節點,因此我們可以放心釋放該節點的資源。
因此,以下新增了一個 list_del_node 函式,基本想法是:在呼叫 list_del_node 時我們雖然是 writer thread ,但我們在利用 atomic operation 做完節點的移除動作後,該節點已無法從 list head 追蹤到,只剩下移除前拿到的人可以繼續使用(~~信賴保護原則~~?),若將我們自己也標記為 reader section (via rcu_read_lock()) ,然後把 zombie node 的 zombie 設定成要刪除的節點,再直接呼叫 rcu_read_unlock() ,這樣等於將 writer thread 也加入 zombie list。
之後若有人退出 reader section 且為最後一個 reader,則會順手執行 free 的動作。
```c=
bool list_del_node(rcu_list_t *list, void *data, finder_func_t finder,
write_handle_t *handle)
{
/* find node and delete it, but defer the free
* until our thread is the last reader thread */
lock_for_write(list);
iterator_t iter = list_find(list, data, finder, handle);
if (!iter.entry) {
unlock_for_write(list);
return false;
}
/* remove the node */
__atomic_store(&iter.entry->prev->next, &iter.entry->next, __ATOMIC_SEQ_CST);
__atomic_store(&iter.entry->next->prev, &iter.entry->prev, __ATOMIC_SEQ_CST);
unlock_for_write(list);
/* add ourself as a reader thread to mark the boundary */
rcu_read_lock(handle);
handle->zombie->zombie = iter.entry;
rcu_read_unlock(handle);
return true;
}
```
相關程式碼修改:執行過程中 Thread Sanitizer 沒有吐出警告訊息
為方便 delete node 的實作,將 list head及list tail 改成預設即是dummy node,且在初始化時,即互連。
https://github.com/conc95xh/linux2021-summer-quiz/blob/main/list/rcu_list.c
## 其他相關閱讀
https://developers.redhat.com/blog/2016/01/14/toward-a-better-use-of-c11-atomics-part-1#
http://www2.rdrop.com/~paulmck/RCU/