2017q1 Team8(B07: phonebook-concurrent) === contribute by <`Cayonliow`、`king1224`、`csielee#`> ###### tags: `sysprog2017` [github](https://github.com/sysprog2017team8/phonebook-concurrent) / [youtube 4/25](https://www.youtube.com/watch?v=yPdjzWUlizk) / [yotube 5/1](https://www.youtube.com/watch?v=F9hKspTdSQ0) ## 當周 youtube {%youtube F9hKspTdSQ0 %} ## 上課問題 2017/5/24 :::info 1. 5分31秒 - `__sync` 開頭是舊版,不推薦使用。建議使用 C11 `__Atomic` 2. 6分39秒 - append 的程式碼有極大的優化空間,可以縮減 while 迴圈的範圍 3. 7分07秒 - 注意 lockfree 標記問題 4. 總結 - 先建立執行緒再進行測試 - 注意正確性 - code 很醜就要改 - 參考論文:[A Pragmatic Implementation Of Non-Blocking Linked-Lists](https://timharris.uk/papers/2001-disc.pdf) - 參考筆記:[Concurrent Linked List 研究](https://hackmd.io/s/rkhBeeZyx) ::: ## 上課紀錄 2017/5/24 :::success - lock_if.h 的 if 是 interface 的意思 - 重新定義 uint32_t ptlock_t 是為了方便可以更換成 pthread_mutex 等等其他 lock 的實作 - static inline 是個特別的方法,能夠將 function 展開,就像 macro 的效果 - 兩大考量 - 程式優雅度 -- 好看好讀 - 型態檢查 -- 用 macro 不會幫忙檢查 ::: ## 任務要求 - 比照 [B07: phonebook-concurrent](https://hackmd.io/s/rkOExTOoe) 要求,實作出刪除 (remove) 特定資料的功能,並且分析 concurrent insert/delete 對效能的影響。留意 race condition 和正確性議題。 ## 討論紀錄 2017/04/15 - 創造 github - 研究 concurrent linked-list lock/lock-free - 初步重構 - 實作 concurrent linked-list lock/lock-free 2017/04/21 - 確認實作方向,先完成 phonebook 使用 append / remove - 下週一錄影 - 持續實作 2017/04/24 - 實作 lock/lockfree 的 Remove 功能 - 撰寫 hackmd - 錄製影片 ## Concurrent linked-list 在多執行緒當中執行 linked-list 的 Append / Remove 會有 Race condition 的問題 主要是因為,當不同執行緒同時對某一個 node 進行操作 順序的不同,可能導致不同結果 這樣就不符合我們的期望 也可以說,在 linked-list 當中的 node 資源具有排他性 因此執行緒之間需競爭( race ) 底下是 Race condition 的一個例子 當 2 個 thread 都想 Append ```sequence participant ThreadA participant ThreadB Note over ThreadA: node *tmp = new_node(); Note over ThreadB: node *tmp = new_node(); Note over ThreadA: list->tail->next = tmp; Note over ThreadB: list->tail->next = tmp; Note over ThreadB: list->tail = tmp; Note over ThreadA: list->tail = tmp; ``` 可以看到因為同時操作 node 會導致 linked-list 的結構造成錯誤 為了避免這樣的競爭造成錯誤 有了 lock(mutex) 的機制讓同一個資源只能夠讓一個執行緒使用 但是 lock(mutex) 在實作上要非常小心,因為不當的操作都有可能造成 deadlock 因此有了 lock-free 這樣的想法 不使用 lock(mutex) ,改用底層的 atomic opreation 將底層可能產生的錯誤用 atomic opreation 處理掉 也就是在一段執行中不會被打斷/分割,就像是最基本原子不可被分割 但是在程式撰寫上就要考慮到資源是不是已經被其他執行緒處理過 而要不要重新執行 ## lock ### 介紹 lock 保護 linked list 的手法就是讓一個區塊的資料,同時只能有一個 thread 進入進行修改或存取。 好像門的概念,當一條 thread 進入這個區塊 並 lock 起來,像是把門關上,其他 threads 必須在門外等候,直到門被打開 (unlock),第二條 threads 才能進入 可是要注意的是第二條 threads 進入以後也會把門關上, 其餘的 threads 需要繼續在門外等待。 也就是說,每次只能有一條 thread 在 lock 與 unlock 之間的區塊。 lock [實作方法](https://github.com/jserv/concurrent-ll/tree/master/src/lock) ```clike typedef struct __PHONE_BOOK_ENTRY { char *lastName; struct __PHONE_BOOK_ENTRY *pNext; pdetail dtl; ptlock_t *lock; } entry; ``` ```clike //LOCK static inline uint32_t lock_lock(volatile ptlock_t *l) { while (CAS_U32(l, (uint32_t)0, (uint32_t)1) == 1) ; return 0; } ``` ```clike //UNLOCK static inline uint32_t lock_unlock(volatile ptlock_t *l) { *l = (uint32_t)0; return 0; } ``` lock 的執行時間會相較長,因爲在同一塊區塊只能有一條thread 在工作, 所以會有其餘的 threads 在等待。 因爲有等待的時間,所以執行時間會變得很久 ## lock-free ### 介紹 若使用 lock 來保護住資料,若同時多個 thread 要對同一資料做改變,當一個 thread 將資料 lock 住之後,其餘所有需要此資源的 thread 都會停等下來,等待資源被 unlock。 因此有 lock-free 的方法出現,當資源無法使用時,thread 也不會卡在那邊不做事,在某些情況下可以更有效率的利用 cpu cycle。 lock-free [實作方法](http://novoland.github.io/%E5%B9%B6%E5%8F%91/2014/07/26/Lock-Free%20%E7%AE%97%E6%B3%95.html) * 自旋 * 以無限迴圈包住 * 在成功達成目的之前不斷嘗試 * 若失敗則繼續嘗試 * 觀察新值、舊值 * 對於想要更新的對象先取得舊值 * 算出新值 * CAS (compare and swap) * compare 要更動的變數是否仍為舊值 * 若為舊值則可以更新為新值 * 若已被更動過則從新計算新值 ### 如何確保 CAS 成功 - Atomic Builtins 可以利用標準函式庫中支援的原子操作,即使在多線程的情況下,這些操作都會保證正確性,而 CAS 則在 [Atomic Builtins for memory access](https://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html) 的歸類中 主要用到的函式是: `type __sync_val_compare_and_swap (type *ptr, type oldval, type newval)` 將一個要更新的變數傳入,同時傳入舊值與新值,若變數內仍為舊值,則更新為新值,若變數內已不是舊值,則不做任何更動,且不論何種情況都會回傳舊值。 範例: ```clike= int main(){ int a,b,c,d; a = 3; b = 5; c = 1; d = __sync_val_compare_and_swap(&a, b, c); printf("%d %d %d %d\n",a,b,c,d); b = 3; d = __sync_val_compare_and_swap(&a, b, c); printf("%d %d %d %d\n",a,b,c,d); return 0; } ``` results: ``` 3 5 1 3 1 3 1 3 ``` ### 過程 在對[ lockfree 範例程式碼](https://github.com/jserv/concurrent-ll/tree/master/src/lockfree)略懂略懂之後,本實作經歷了三個主要階段 * 我相信我可以 * 區區一個 link-list,一定不用半天就可以搞定了 * 但 Bug 多到不知道從哪裡開始找起 * 依樣畫葫蘆 * 還是模仿 ~~(照抄)~~ 範例程式碼中的實作方式 * 成功朝著夢想一步一步邁進 * 感覺差最後一步時卡住半天 * 認真學習 * 從頭檢視並理解程式碼詳細實作原理 * 再無限的 `core dump` 之後,今天開始學著使用 [GDB](https://www.puritys.me/docs-blog/article-329-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-GDB-Debug.html) 指令,~~之前只查到進階指令,看不懂就馬上關掉了~~ * 在 gdb 的幫助下,果然成功了呢! ### 程式碼 [csielee 大大](https://github.com/orgs/sysprog2017team8/people/csielee) 先用 [csielee](https://github.com/csielee/phonebook-concurrent) 的方式幫我們整理重構了原始程式碼,因此需要改變的部份主要分成三個函式 * search 在整個 link-list 中搜尋目標 entry,若有已被刪除的函數也在這裡移除鏈結,在 remove 中僅做標記,回傳目標 entry 或 link-list 的尾巴,並將其中一個 call by reference 的 entry 參數設成目標之前一項,方便 caller 使用 ```clike= while( is_marked_ref(j_next) || strncasecmp(str,j->lastName,len)!=0 ) { if(!is_marked_ref(j_next)) { __sync_val_compare_and_swap(&tmpHead,tmpHead,j); (*left_entry) = j; left_next = j_next; } j = get_unmarked_ref(j_next); if(j == entrytail) break; j_next = j->pNext; } ``` 若該項被標記或與目標不符,則繼續往後搜尋,且每次需拿未標記的位址,否則對標記過得位址存取 data 會 core dump 而其中 strncasecmp 函式只能比較前 len 個字元,當一個字串恰好是另一個的前綴時該怎麼辦? ```clike= int len = strlen(str); if(strlen(j->lastName) == len) break; ``` 再包上一層迴圈,若 strncasecmp 回傳 0 且兩字串長度相等,才能確定兩字串是真正完全一樣而離開迴圈 ```clike= if(left_next == right) { if(!is_marked_ref(right->pNext)) return right; } else if(__sync_val_compare_and_swap(&((*left_entry)->pNext), left_next, right) == left_next ) { if(!is_marked_ref(right->pNext)) { return right; } } ``` left 為目標前一項,right 為目標,left_next 與 right 相等則表示資料沒被更動過,反之則代表中間有很多被標記為刪除的資料,則將 right 接到 left 之後 * append 將新的 data 接到 link-list 末端,因總資料量太大,因此無檢查是否有重複 ```clike= while(1) { right = search(i, &left, tmpHead); if(strncasecmp(right->lastName,i,len) == 0 && strlen(right->lastName) == len) break; j->pNext = right; if(__sync_val_compare_and_swap(&(left->pNext), right, j) == right) break; } ``` 若得到的目標資料已存在,則不做任何事離開,但我在這邊傳入的 link-list entry 非 link-list 的 head,因此在 search 時並不是從頭開始檢查,即使有重複也檢查不出來 ( right 永遠是 link-list tail ) 若成功更新後就離開迴圈,否則重新計算新值,取得 tail 的前一項 * remove 搜尋目標並在刪除後回傳 1,若搜尋不到則不做任何事並回傳 0 ```clike= while(1) { right = search(lastName, &left, entryHead); if(right == NULL || strncasecmp(right->lastName,lastName,len) != 0){ return 0; } right_succ = right->pNext; if(!is_marked_ref(right_succ)) { if(__sync_val_compare_and_swap(&(right->pNext), right_succ, get_marked_ref(right_succ)) == right_succ){ return 1; } } } ``` 將目標的 pNext 中的資料標記起來 參考: [SMP中多线程程序的性能衰退现象之False Sharing](http://www.voidcn.com/blog/yockie/article/p-6328160.html) [Frank Tan's Blog](http://www.cnblogs.com/FrankTan/archive/2010/12/11/1903377.html) ## 分析 |結構|執行方式|資料來源| |:---:|:---:|:---:| |linked-list|多執行緒<br>分區塊執行|append:人名資料<br>remove:前一萬筆人名| |concurrent linked-list<br>lock|多執行緒<br>分區塊執行|append:人名資料<br>remove:前一萬筆人名| |concurrent linked-list<br>lock-free|多執行緒<br>分區塊執行|append:人名資料<br>remove:前一萬筆人名| ### 執行狀況 因為我們想要專注在 concurrent linked-list 上對效能的影響 因此我們都傳入已經對齊好的文字檔案 並且都一次 malloc 好需要的記憶體,讓效能影響的變因減少 能夠好好研究 concurrent linked-list 的效能議題 <br> - 呈獻在不同 linked-list 下的 AppendByFile / RemoveByFile 時間 :::warning 因為在 linked-list 難以實作多執行緒的刪除 因此時間均為 0 ::: ![](https://i.imgur.com/UcTLasU.png) :::info 觀察後發現 在 Append ,lock / lockfree 都有上升 推測是因為要處理 race condition 的問題 因此執行緒無法全力執行,必定有等待的時間 <br> 而在 Remove lock 的 Remove 時間異常的長 lockfree 的 Remove 時間也比 Append 來得長 這部份感覺還需要調整一下 跟預期的想像不一樣 ::: ## lock-free 優化部份 主要優化了兩個部份 * thread_args * 針對 thread_args 使用 memory pool 來實作,一次取得所有會用到的空間,減少 system lock 的情況 * append * 將 append 中一些不需要的程式碼移除,讓各 threads 可以從任何地方 append,不用彼此搶著從同一個地方 append,減少 CAS 的失敗率 #### thread_args memory pool ```clike thread_args_pool = (thread_arg *) malloc(sizeof(thread_arg) * THREAD_NUM); ``` 一次將所需要的空間 malloc 出來,減少 malloc 的呼叫次數 ```clike thread_args[i] = createThread_arg(map + MAX_LAST_NAME_SIZE * i,map + file_size, i,THREAD_NUM, entry_pool + i, thread_args_pool + i); ``` 在 create 新的參數時,傳入事先取得的空間直接使用 ```clike= static thread_arg *createThread_arg(char *data_begin, char *data_end, int threadID, int numOfThread, entry *entryPool,thread_arg *argPool) { argPool->data_begin = data_begin; argPool->data_end = data_end; argPool->threadID = threadID; argPool->numOfThread = numOfThread; argPool->lEntryPool_begin = entryPool; argPool->lEntry_head = argPool->lEntry_tail = entryPool; return argPool; } ``` 從 memory pool 直接取一條 thead_arg 出來使用,取代每次都重新 malloc 空間的方法 #### append ```clike= static void append(void *arg) { thread_arg *t_arg = (thread_arg *) arg; int count = 0, len; entry *left, *right,*tmpHead; entry *j = t_arg->lEntryPool_begin; tmpHead = entryHead; for (char *i = t_arg->data_begin; i < t_arg->data_end; i += MAX_LAST_NAME_SIZE * t_arg->numOfThread, j += t_arg->numOfThread, count++) { left = right = NULL; if(i[strlen(i)-1]=='\n') i[strlen(i)-1] = '\0'; j->lastName = i; while(1) { right = tmpHead->pNext; j->pNext = right; if(__sync_val_compare_and_swap(&(tmpHead->pNext), right, j) == right) break; } tmpHead = j; DEBUG_LOG("thread %d t_argend string = %s\n", t_arg->threadID, new -> lastName); } pthread_exit(NULL); } ``` 每個 threads 都是從 entryHead 開始,但各個 threads 會不斷的將要插入的位置更新為自己上一次插入的 entry 之後,而每個 threads 插入的 entry 位置都不同,因此任兩個 threads 幾乎不會有同時搶著對同一個位置做改變的情況 <br> ## 遇到問題 - 有 memory leak 的問題 - lock 的刪除有正確性的問題 - 無法充份表現 concurrent linked-list - 嘗試用 ThreadSanitizer 去驗證正確性議題 - lock 可以嘗試改用 pthread_mutex <br> ## interface Refactor :::info TODO ::: 統一 lniked-list 的介面 ```clike insertEach(entry *new_element); removeEach(char *name); getuserdata(char *name); ``` <!-- 網頁 CSS 樣式 --> <style> .markdown-body h2 { padding-top : 0.15em; padding-bottom : 0.15em; padding-left : 0.2em; background-color : DarkGrey; border: 1px solid transparent; border-radius: 4px; } .markdown-body { } </style>