# 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/