# 2021q3 Homework2 (hp) contributed by < `demonsome` > ###### tags: `2021 Summer - Linux kernel` > [第 2 週測驗題題目](https://hackmd.io/@sysprog/linux2021-summer-quiz2) ## 延伸問題 **1. 解釋上述程式碼運作原理** 我們先從主程式開始: ```c= int main(void) { list_t *list = list_new(); pthread_t thr[N_THREADS]; for (size_t i = 0; i < N_THREADS; i++) pthread_create(&thr[i], NULL, (i & 1) ? delete_thread : insert_thread, list); for (size_t i = 0; i < N_THREADS; i++) pthread_join(thr[i], NULL); for (size_t i = 0; i < N_ELEMENTS; i++) { for (size_t j = 0; j < tid_v_base; j++) list_delete(list, (uintptr_t) &elements[j][i]); } list_destroy(list); fprintf(stderr, "inserts = %zu, deletes = %zu\n", atomic_load(&inserts), atomic_load(&deletes)); return 0; } ``` 我們可以發現主程式一開始建立了一個 list 後,再創造了64個 [POSIX thread](https://www.cs.cmu.edu/afs/cs/academic/class/15492-f07/www/pthreads.html) (pthread) 。提到 pthread ,這種執行緒可在多個核心並行以提升程式執行效能。然而,並行程式設計的挑戰在於多個運算核心對共享資源的存取的問題。 這次的作業探討了 lock-free 的 hazard pointer 結構在並行程式上執行的可行性。這裡建立 pthread 是為了測試這個使用 hazard pointer 的 list 結構。檢視它的新增節點 (insert) 、刪除節點 (delete) 計數是否一致。 **結構 Harzard pointer list 初始化** 初始化 list 的函式如下: ```c= list_t *list_new(void) { list_t *list = calloc(1, sizeof(*list)); assert(list); list_node_t *head = list_node_new(0), *tail = list_node_new(UINTPTR_MAX); assert(head), assert(tail); list_hp_t *hp = list_hp_new(3, __list_node_delete); atomic_init(&head->next, (uintptr_t) tail); *list = (list_t){.hp = hp}; atomic_init(&list->head, (uintptr_t) head); atomic_init(&list->tail, (uintptr_t) tail); return list; } ``` list 初始化時,創造兩個 node ,其一是 head node 其 key 值是0,另一個是 tail node 其 key 值是 `uintptr_t` 的最大值 ( $2^{16}$ -1 , or higher)。此外,建立了一個 hazard pointer 的 list (struct `list_hp_t `)。最後,以 atomic 的方式初始化 head 與 tail 指標,以及 head node 與 tail node 之間的連結。 我們進一步去檢查建立 `list_hp_t` 的函式 `list_hp_new()` 如下: ```c= list_hp_t *list_hp_new(size_t max_hps, list_hp_deletefunc_t *deletefunc) { list_hp_t *hp = aligned_alloc(128, sizeof(*hp)); assert(hp); if (max_hps == 0) max_hps = HP_MAX_HPS; *hp = (list_hp_t){.max_hps = max_hps, .deletefunc = deletefunc}; for (int i = 0; i < HP_MAX_THREADS; i++) { hp->hp[i] = calloc(CLPAD * 2, sizeof(hp->hp[i][0])); hp->rl[i * CLPAD] = calloc(1, sizeof(*hp->rl[0])); for (int j = 0; j < hp->max_hps; j++) atomic_init(&hp->hp[i][j], 0); hp->rl[i * CLPAD]->list = calloc(HP_MAX_RETIRED, sizeof(uintptr_t)); } return hp; } ``` 程式初始化了一個對齊128的 `list_hp_t` ,而 `list_hp_t` 的結構如下: ```c= 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; }; ``` 在這個例子,`max_hps` 會被設成3,而指標 `deletefunc` 指向 `__list_node_delete()` ,一個釋放節點的函式。 此外,此結構中包含了一個二維的指標陣列 `hp`,即 Hazard pointer(s) 。以及一個二維的指標陣列 `rl`,即 Retire list 。 其中,指標陣列 `hp` 的第二個維度是最大的 thread 數,也就是說, 這裡分配給每個 thread 一串 Hazard pointers。 **pthread 的建立** 主程式中 [`pthread_create`](https://man7.org/linux/man-pages/man3/pthread_create.3.html) 函式創造 pthread ,並將指標 `list` 作為引數傳入`delete_thread()` 「或」`insert_thread()` 起始常式 (start routine)。注意到這個起始常式是由 i 值的奇偶決定的。當 i 是奇數,起始常式是 `delete_thread()` 否則是 `insert_thread()` 。 ```c= pthread_create(&thr[i], NULL, (i & 1) ? delete_thread : insert_thread, list); ``` 這兩個 start routine 分別呼叫了 `list_insert()` 及 `list_delete()` 對 list 進行128個節點的插入以及刪除。2個函式都需要2個引數,一個是指標 `list` ,另一個是 key 值,程式碼如下: ```c= static void *insert_thread(void *arg) { list_t *list = (list_t *) arg; for (size_t i = 0; i < N_ELEMENTS; i++) (void) list_insert(list, (uintptr_t) &elements[tid()][i]); return NULL; } static void *delete_thread(void *arg) { list_t *list = (list_t *) arg; for (size_t i = 0; i < N_ELEMENTS; i++) (void) list_delete(list, (uintptr_t) &elements[tid()][i]); return NULL; } ``` 注意到插入/刪除的節點以 `(uintptr_t) &elements[tid()][i]` 作為 key 值。 `tid()` 返回值作為 `element` 這個靜態二維陣列的索引值。 `tid` 相關程式碼如下: ```c= #define TID_UNKNOWN -1 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_v` 變數被 `static thread_local` keyword 所修飾,代表 `tid_v` 只有在各自的 thread 中看的到,他的生命週期侷限在 thread 的出生到死亡之間。且因為是靜態變數,只會初始化一次。 `tid()` 被呼叫時,如果 `tid_v` 仍然是初值,就把它改成 `tid_v_base` 加一,這表示每一個 thread 只會對 `tid_v_base` 加一並指派給 `tid_v` 。 總而言之,當我們創造了64個並行的 pthread , pthread 會由插入/刪除節點的 start routine 開始執行。接著,每個 thread 呼叫 `tid()` 對 `tid_v_base` 靜態變數以 atomic 的方式加一,每個 thread 只會執行一次加一。其後, `tid_v_base` 的值會被回傳,作為 `element` 指標陣列的索引值。 `element` 被 dereferenced 後的位址傳入 `list_delete()` 及 `list_delete()` 作為 key 值。 **list 節點的插入/刪除** <<[Lock-Free Data Structures](http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=B2561FF75B93F8C70CC44DF6079CB81B?doi=10.1.1.98.3363&rep=rep1&type=pdf)>> 文中第5章 "Garbage Collector, Where Are Thou?" 提到了 lock-free 的基本概念。演算法對於 Map (指的是共享資源) 的 lookup function 不實作 lock,而對 Map 的 update function 實作 CAS operation ,如此以確保 Map 的更新是 atomic 的。因此,當我們要刪去舊的 Map 時,需要有一個 garbage collector 來檢查舊的 Map 有沒有正在被其他 thread lookup。只有確保沒人 lookup 時,我們才能刪除舊 Map 。 <<[Lock-Free Data Structures with Hazard Pointers](https://erdani.org/publications/cuj-2004-12.pdf)>> 文中則提到 Hazard pointer 結構的實作方式,這裡整理了一些重點: * 使用 Hazard pointer 結構的前提是 Write-Rarely-Read-Many * Hazard pointer 結構能滿足 wait-free * 每個 reader thread 擁有一個 Hazard pointer 。當 Hazard pointer 指向一個 Map,代表該 Map 可能有 thread 正在讀取,可以 remove 它,但不能 delete 它 * 每個 thread 有自己的 private list 叫做 retire list。這個 retire list 存放了被取代的 Map * Retire list 中被取代 Map 累積到一定數量時, thread 會去檢查 (scan) 這些 Map 有沒有被所有 thread 的 Hazard pointer 指向。若沒有,才可以刪除 回到程式實作,因為 thread 在共享的 list (論文中指的 Map) 上新增或刪除節點,都是 update operaion ,故都要先對 Hazard pointer 進行 scan 。以下是 `list_insert()` 程式碼: ```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; } } } ``` 我們可以發現 `__list_find()` 相當於上述對 retire list 執行 scan 的動作,一旦發現要插入的節點被某個 thread 的 Hazard pointer 指向,則將插入的節點刪除並返回。否則,將節點加入 list 中。 **2. 指出改進空間並著手實作** 先來看一下 list.c 的執行時間: ``` time ./list inserts = 4098, deletes = 4098 real 0m28.477s user 0m28.316s sys 0m0.060s ``` 總執行時間約為28秒,主要由這個使用者程序的使用的 CPU 時間佔據。 觀察 `__list_find()` 函式中的片段如下,我們可以發現搜尋是依 linked list 的順序往下找的。雖然 hazard pointer 的數量是固定的,但是在多層 loop 下執行還是相當耗時。 故我打算參考 <<[Lock-Free Data Structures](http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=B2561FF75B93F8C70CC44DF6079CB81B?doi=10.1.1.98.3363&rep=rep1&type=pdf)>> 文中第2章來實作 hashtable ,將 hazard pointer 結構化,以降低搜尋的 overhead。 ```c= 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)) goto try_again; 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; 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 get_unmarked_node(curr)->key == *key; } 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 (!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; ``` **3. 對比 rcu_list,解釋同為 lock-free 演算法,跟上述 Hazard pointer 手法有何異同?能否指出 rcu_list 實作缺陷並改進?**