# 2021q3 Homework2 (hp) contributed by < `ccs100203` > ###### tags: `linux2021q3` > [quiz2](https://hackmd.io/@sysprog/linux2021-summer-quiz2) - reference [Lock-Free Data Structures with Hazard Pointers](https://erdani.org/publications/cuj-2004-12.pdf) [A Pragmatic Implementation of Non-Blocking Linked-Lists](https://www.cl.cam.ac.uk/research/srg/netos/papers/2001-caslists.pdf) [並行程式設計: Lock-Free Programming](https://hackmd.io/@sysprog/concurrency/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Fconcurrency-lockfree) [並行程式設計: Atomics 操作](https://hackmd.io/@sysprog/concurrency/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Fconcurrency-atomics) - source [hp_list](https://github.com/sysprog21/concurrent-programs/tree/master/hp_list) [concurrent-ll](https://github.com/jserv/concurrent-ll) ## 解釋程式碼運作原理 ![](https://i.imgur.com/B221sjs.png) 程式以 lock-free 以及 hazard pointer 的手法,建立相同量的 thread 做 insert / delete 的動作,上圖為 hazard pointer 的結構示意。 ### Struct ```cpp typedef struct { int size; uintptr_t *list; } retirelist_t; ``` Hazard pointer 中使用到的 retire list 的結構 ```cpp #define CLPAD (128 / sizeof(uintptr_t)) typedef struct list_hp list_hp_t; typedef void(list_hp_deletefunc_t)(void *); struct list_hp { int max_hps; alignas(128) atomic_uintptr_t *hp[HP_MAX_THREADS]; alignas(128) retirelist_t *rl[HP_MAX_THREADS * CLPAD]; list_hp_deletefunc_t *deletefunc; }; ``` 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](https://erdani.org/publications/cuj-2004-12.pdf) 中描述。 - [alignas](https://en.cppreference.com/w/cpp/language/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 ```cpp typedef uintptr_t list_key_t; typedef struct list_node { alignas(128) uint32_t magic; alignas(128) atomic_uintptr_t next; list_key_t key; } list_node_t; /* Per list variables */ typedef struct list { atomic_uintptr_t head, tail; list_hp_t *hp; } list_t; ``` 此程式的結構都包裝在 `list_t` 之內,以其為 list 的主體。 - `magic` 協助驗證正確性,確保該 node 的記憶體範圍並未被其他部份修改到。 - `key` 是 list 與 hazard pointer 中用來索引的 index,會以 tid 為依據。 ### Tid ```cpp static thread_local int tid_v = TID_UNKNOWN; static atomic_int_fast32_t tid_v_base = ATOMIC_VAR_INIT(0); static inline int tid(void) { if (tid_v == TID_UNKNOWN) { tid_v = atomic_fetch_add(&tid_v_base, 1); assert(tid_v < HP_MAX_THREADS); } return tid_v; } ``` 程式利用 tid,以遞增的方式對每條 thread 進行編號,存放在具有 [thread_local](https://www.gnu.org/software/libc/manual/html_node/ISO-C-Thread_002dlocal-Storage.html) 性質的 `tid_v` 中,代表該變數在每個 thread 具有獨立性,並有 thread storage duration。 `atomic_int_fast32_t` 這個 type 在 [std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) 及 [Fixed width integer types](https://en.cppreference.com/w/cpp/types/integer) 之中有解釋,在 C++ 11 後有分成 `int8_t`, `int_fast8_t`, `int_least8_t`。 - `int8_t` 剛好 8 bit 的 int - `int_fast8_t` 進行操作最快的 int,但是至少 8 bit - `int_least8_t` 該 int 使用最少的 bit,但是至少 8 bit ### Find `__list_find` 是此程式的核心,這裡會以 lock-free 的手法,並讓 list 維持 ascending,以及讓某些條件下的 node 放入 retire list。回傳 true 代表找到該 `key` 在 list 內,並藉由 pointer to pointer 的方式回傳給 caller。 ```cpp= 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; 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)) { TRACE(retry); goto try_again; } ``` - `prev` 的作用像是存放 `curr` 的複本,用來對照當前的 `curr` 是否已經被更改,是以 pointer to pointer 的方式。 - `try_again:` 的作用是當程式發現當前操作的指標有被其他 thread 更動過時,就會重做 - `list_hp_protect_ptr` 會將操作的節點放入 hazard pointer 中,代表當前的 thread 正在使用,作為避免被刪除的保護 - 第 14~17 行,確認此時的 `prev` 與 `curr` 是否相同,不同就要重做 :::info 2021/08/20 可以確認 `head` 是否被 mark ::: ```cpp=18 while (true) { if (is_marked(curr)) TRACE(contains); next = (list_node_t *) ATOMIC_LOAD(&get_unmarked_node(curr)->next); (void) list_hp_protect_ptr(list->hp, HP_NEXT, get_unmarked(next)); /* On a CAS failure, the search function, "__list_find," will simply * have to go backwards in the list until an unmarked element is found * from which the search in increasing key order can be started. */ if (ATOMIC_LOAD(&get_unmarked_node(curr)->next) != (uintptr_t) next) { TRACE(retry); goto try_again; } if (ATOMIC_LOAD(prev) != get_unmarked(curr)) { TRACE(retry); 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 (get_unmarked_node(curr)->key == *key); // FFF } prev = &get_unmarked_node(curr)->next; (void) list_hp_protect_release(list->hp, HP_PREV, get_unmarked(curr)); } else { uintptr_t tmp = get_unmarked(curr); if (!CAS(prev, &tmp, get_unmarked(next))) { TRACE(retry); 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)); TRACE(traversal); } } ``` 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](https://www.cl.cam.ac.uk/research/srg/netos/papers/2001-caslists.pdf) 的手法,被標記的 memory 區塊代表他是已經準備被刪除的了,所以就不會將此 `next` 作為要使用的下一個 node。 - 已經被 mark 的節點,會在第 45 行的 else,先通過 [CAS](https://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.html) 確定還是當前的 `next`,然後用 `list_hp_retire` 將他放入 retire list裡,並進行處理。 - 未被 mark 的節點,會先透過 `(get_unmarked_node(curr)->key < *key)` 確認是否符合條件,要注意第 40 行的判斷,會回傳 true 讓程式不插入相同 `key` 值的節點。 - 使用 `list_hp_protect_release` 將需要保護的 node 放入 hazard pointer 之中,利用 release 的 memory order 確保在這之前的 store 都以寫入。 ### Insert ```cpp= 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 (CAS(prev, &tmp, (uintptr_t) node)) { list_hp_clear(list->hp); return true; } TRACE(ins); } } ``` 第 6 行會先利用 `list_node_new` 根據傳入的 `key` 創一個新的 node,藉由 `__list_find` 得到插入的位置,要注意回傳 true 則代表有重複節點,所以把現在新創立的 destroy 並從 hazard pointer 中移除。 再來程式會將新的 `node` 的 `next` 先指向 `curr` ```graphviz digraph graphname{ node[shape=box] curr[shape=plaintext,fontcolor=red] prev[shape=plaintext,fontcolor=blue] rankdir=LR { rankdir = LR A[label=A] B[label=B] C[label=C] D[label="node"] NULL1[label=NULL,shape=plaintext] // NULL2[label=NULL,shape=plaintext] } { rank="same"; // indirect[shape=plaintext,fontcolor=blue] prev->curr->B } A->B->C->NULL1 D->B } ``` 再來先透過 CAS 確保 `prev` 的位置還未被換過,然後將 `node` 放入原本 `curr` 的位置中 ```graphviz digraph graphname{ node[shape=box] curr[shape=plaintext,fontcolor=red] prev[shape=plaintext,fontcolor=blue] rankdir=LR { rankdir = LR A[label=A] B[label=B] C[label=C] D[label="node"] NULL1[label=NULL,shape=plaintext] } { rank="same" curr->B } { rank="same" prev->D } A->D->B->C->NULL1 } ``` #### list_node_new ```cpp list_node_t *list_node_new(list_key_t key) { list_node_t *node = aligned_alloc(128, sizeof(*node)); assert(node); *node = (list_node_t){.magic = LIST_MAGIC, .key = key}; (void) atomic_fetch_add(&inserts, 1); return node; } ``` 利用 [aligned_alloc](https://linux.die.net/man/3/aligned_alloc) 確保配置的空間對齊 128。 使用 [atomic_fetch_add](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html) 操作 `inserts`,紀錄插入的次數。 ### Delete ```cpp= 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 (!CAS(&curr->next, &tmp, get_marked(next))) { TRACE(del); continue; } tmp = get_unmarked(curr); if (CAS(prev, &tmp, get_unmarked(next))) { list_hp_clear(list->hp); list_hp_retire(list->hp, get_unmarked(curr)); // DDD } else { list_hp_clear(list->hp); } return true; } } ``` 用 `__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` 去處理。 ### Retire 在 [Lock-Free Data Structures with Hazard Pointers](https://erdani.org/publications/cuj-2004-12.pdf) 中有提及 retire list 的原理,當 list 內的東西達到一定量時,在這邊是利用 `HP_THRESHOLD_R`,會去查找所有 hazard pointer 的內容,確認要刪除的 memory 不在 hazard pointer 的保護中,來判斷 retire list 內的記憶體是否可以釋放掉。 ```cpp= 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; ``` 將傳入的 `ptr` 放入該 thread 對應的 retire list 中。 再藉由 `HP_THRESHOLD_R` 去判斷是否要開始進行回收的動作,因為程式的 `HP_THRESHOLD_R` 設為 0,所以每次有節點放入就會立刻進行查找的動作。(==或許可以參照論文的建議將其調大==) ```cpp=9 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); // RRR } } } ``` 第 9 行,會以每一個 retire list 內的節點,去 hazard pointer 中進行查找,如果有找到就會將 `can_delete` 設為 false,代表還不能刪除。 最後如果可以刪除,會利用 [memmove](https://man7.org/linux/man-pages/man3/memmove.3.html) 將 `iret` 之後記憶體空間往前移,覆蓋掉被刪除的位置,藉以達到從 retire list 中移除的效果,同時可以維持該陣列的結構。並將要刪除的節點傳入 `deletefunc` 刪除。(在程式中是 `__list_node_delete`) #### Delete Function ```cpp void list_node_destroy(list_node_t *node) { if (!node) return; assert(node->magic == LIST_MAGIC); free(node); (void) atomic_fetch_add(&deletes, 1); } static void __list_node_delete(void *arg) { list_node_t *node = (list_node_t *) arg; list_node_destroy(node); } ``` `free` 之前,會先對照 `LIST_MAGIC` 是否相同,藉由比對 [magic number](https://en.wikipedia.org/wiki/Magic_number_(programming)) 來判斷該記憶體位置沒有被不正確的更改,保證程式的正確性。 同樣利用 [atomic_fetch_add](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html) 操作 `deletes`,紀錄刪除的次數。 ## 程式改進空間 1. 128 / 2 改成 128 >> 1 2. find 中,try again 到 while 之間驗證 prev 跟 curr 的必要性 3. max_hps 沒檢查,沒做上限 4. memmove 效率? 雖然 intel 有對其優化 5. 論文中有提及 hash table of hazard pointer, hazard pointer / retire list 用其他更方便搜尋的結構跟使用其他搜尋演算法 ## 對比 [rcu_list](https://github.com/sysprog21/concurrent-programs/tree/master/rcu_list),解釋同為 lock-free 演算法,跟上述 [Hazard pointer](https://en.wikipedia.org/wiki/Hazard_pointer) 手法有何異同?能否指出 rcu_list 實作缺陷並改進? TODO