# [phonebook-concurrent](https://hackmd.io/s/rJsgh0na) [github](https://github.com/diana0651/phonebook-concurrent) contributed by <`Diana Ho`> ###### tags: `d0651` `sys` ## 案例分析 * [phonebook](https://hackmd.io/KzDMBYFME4CMHYC00BmLiPAE1gNkQBzTj4CM4ADAIYDGpBpa9QA=) ### 預期目標 * 學習第二週提及的 [concurrency](/s/H10MXXoT) 程式設計 * 學習 POSIX Thread * 學習效能分析工具 * code refactoring 練習 * 探索 clz 的應用 ### 作業要求 *  [phonebook-concurrent](https://github.com/sysprog21/phonebook-concurrent) * 適度修改 `phonebook_opt.c` 和相關的檔案 * 程式正確性驗證 * 效能分析實驗 (必須要有圖表) * 說明你如何改善效能 * 透過 POSIX Thread 縮減 `alloc()` 的時間成本 * 建立 thread pool 管理 worker thread * 學習 [concurrent-ll](https://github.com/jserv/concurrent-ll) (concurrent linked-list 實作) 的 scalability 分析方式 * 透過 gnuplot 製圖比較 list 操作的效能 * 嘗試重構 (refactor) 給定的程式碼 --- ## Toward Concurrency ### Concurrency (並行) vs. Parallelism (平行) * 名詞解釋 * Sequential (順序):一件做完再做另一件 * Concurrency (並行):兩件同時進行 * Parallelism (平行):兩件交錯進行&平行 * [Concurrency is not Parallelism](https://blog.golang.org/concurrency-is-not-parallelism) * concurrency provides a way to structure a solution to solve a problem that may be parallelizable * different concurrent designs enable different ways to parallelize * 比較 * 單核心 CPU:multi-threading 無法達到 parallelism,而只有 concurrency * 多核心 CPU:concurrency 通常具有 parrallel 特性 * [Concurrency 系列文章](http://opass.logdown.com/posts/) * sequeced-before: 對同一個 thread 下,求值順序關係的描述 同一個 thread 內的 happens-before * happens-before: 前一個操作的效果在後一個操作執行之前必須要可見 強調可見,而不是實際執行的順序 * synchronized-with: 跨 thread 版本的 happens-before 兩個不同 thread 間的同步行為 * Memory Consistency Models: 系統保證正確的情況,盡可能最佳化 * Sequential Consistency: 兩個觀點 1. 對於每個獨立的處理單元,執行時都維持程式的順序(Program Order) 2. 整個程式以某種順序在所有處理器上執行 * concurrency 問題: 1. 多執行序競爭同一個資源 2. thread 之間的不同步行為 * 使用 mutex 或 semephore 改善 * 確保在寫入或讀取共享記憶體資源之操作是 atomic 的 * 必要時取消編譯器優化,因為可能發生 memory reordering 的不同步行為 * lock 與 lock-free programming 核心目標:使程序間互相溝通順利 * lock:包含 mutex 與 semephore 等,防止程序競爭同一記憶資源 * lock-free:沒有使用 mutex 與 semephore 等,但能達到 lock 者 * 未來的效能效能提昇方向: 1. hyperthread (多執行序並行處理task) 2. multicore (核心多自然處理的快) 3. cache (cache大小的增加與速度的提昇對效能影響顯著) >>[參考整理](https://hackmd.io/IwIxBYDMDZYWgEwA5oHY7gMwGNtxAKyQCGcAJgKYLgLBnSQEIhA=?both) >>[參考整理](https://hackmd.io/GwEwZgRgHArGAMBaAxgUwgdkQFgEwEZhEBOAQ2WMTAGYRr5VsYZGIg==?both) ## POSIX Threads >[POSIX Threads Programming](https://computing.llnl.gov/tutorials/pthreads/) ### Thread * Thread : 輕量級的行程(Lightweight process;LWP) * Mach microkernel 作業系統中同一個 task 的所有 thread 共享該 task 所擁有的系統資源。 * UNIX 的一個 process 可以看成是一個只有一個 thread 的 Mach task。 * 對 CPU 而言,產生一個 thread 是一件相對簡單的工作。 * 共享系統資源的 thread 跟獨佔系統資源的 process 比起來,thread 是也是相當節省記憶體的。 #### Synchronization ... :::info Thread 同步問題:**mutex** 和 **condition variable** * POSIX 提供了兩組用來使 thread 同步的基本指令: mutex 和 condition variable。 * mutex : * 一組控制共享資源存取的函數 * 一般用在解決 race condition 問題 * mutex 並不是一個很強的機制,因為他只有兩個狀態:locked 和 unlocked * condition variable : * 將 mutex 的功能加以延伸,能夠做到讓某一個 thread 暫停,並等待另一個 thread 的 signal * 總是搭配 mutex lock 使用 ![](http://i.cmpnet.com/ddj/blogs/2011/06/inheritance.png) >>[參考觀念](https://hackmd.io/JwDgzMCMAmAMCGBaYAmaSAsA2ARpRIArAGbSLSGFbzogbTA5A===?both) ::: #### Process vs. Thread vs. Coroutines ### 使用注意事項 * 編譯使用 pthread 的程式 * 引入相關的標頭檔 `<pthread.h>` * 連結 pthread library `-lpthread` ```clike= pthread_t a_thread; pthread_attr_t a_thread_attribute; void thread_function(void *argument); char *some_argument; ``` ### PThread 函式 #### [pthread_setconcurrency](http://man7.org/linux/man-pages/man3/pthread_getconcurrency.3.html) ```clike= int pthread_setconcurrency(int new_level); int pthread_getconcurrency(void); ``` >[UNIX環境高級編程:線程屬性之並行度](http://www.bianceng.cn/OS/unix/201408/44151.htm) #### [pthread_create](http://man7.org/linux/man-pages/man3/pthread_create.3.html) ```clike= int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); ``` #### [pthread_join](http://man7.org/linux/man-pages/man3/pthread_join.3.html) ```clike= int pthread_join(pthread_t thread, void **retval); ``` ### Mutex 函式 >[Mutexes](https://computing.llnl.gov/tutorials/pthreads/#Mutexes) ```clike= pthread_mutex_t mutex; pthread_mutex_init(&mutex, pthread_mutexattr_default);//定義mutex pthread_mutex_lock( &mutex );//鎖定共享資源 pthread_mutex_unlock( &mutex );//解所共享資源 pthread_mutex_destroy();//結束mutex ``` ### Thread Pool >>[參考觀念](https://hackmd.io/GYVgLATAbGAmYFpYHYBGsFgMYEZgNRFgA4CBmVMYATmIENYAGLCIA===?both) - Thread 缺陷 - 建立太多執行緒,會浪費一定資源,並且有些執行緒未被充份使用 - 銷毀太多執行緒,會導致之後浪費時間再次創建它們 - 建立執行緒太慢,導致效能變差 - 銷毀執行緒太慢,導致其它執行緒沒有資源 - Thread Pool 功能 - 管控 Thread 的產生與回收 - 分發 Thread 處理 request - 處理 request 的 queue >[C 的 Thread Pool 筆記](http://swind.code-life.info/posts/c-thread-pool.html) >[github source code](https://github.com/mbrossard/threadpool) ```clike= typedef struct { void (*function)(void *); void *argument; } threadpool_task_t; //Thread Pool 需要 job/task 讓 Thread 知道他們要做什麼事情 threadpool_t *threadpool_create(int thread_count, int queue_size, int flags); //傳入 thread 的數量,queue 的大小,回傳 threadpool 型態是 threadpool_t int threadpool_add(threadpool_t *pool, void (*routine)(void *), void *arg, int flags); //傳入 threadpool,要執行的 function 位置,要傳入的參數 int threadpool_destroy(threadpool_t *pool, int flags); //傳入 threadpool,會把 threadpool 的空間 free 掉 ``` ### Lock-free Thread Pool #### Thread Pool VS Lock-Free Thread Pool * Thread Pool ![](https://hackpad-attachments.s3.amazonaws.com/embedded2016.hackpad.com_VJmq0R0ILi6_p.537916_1459234875775_001.PNG) 共享 workqueue 的操作必須在 mutex 的保護下安全進行。 * Lock-Free Thread ![](https://hackpad-attachments.s3.amazonaws.com/embedded2016.hackpad.com_VJmq0R0ILi6_p.537916_1459234893887_002.PNG) * 解決 lock 機制: 解決 lock-free 必須避免共享資源的競爭,因此將共享 workqueue 拆分成每個 worker thread 一個 workqueue。 對於 main thread 放入工作和 worker thread 取出任務的競爭問題,可以採取 ring-buffer 的方式避免。 * condition variable 問題: condition variable 解決執行緒之間的通訊。 semaphore 作為一種通信方式,可以代替之。 >[高效線程池之無鎖化實現](http://blog.csdn.net/xhjcehust/article/details/45844901) ```clike= void *tpool_init(int num_threads) //初始化時決定要用哪種方式進行 put work,選用 `ring-buffer` 的方式將 worker 加入queue static int wait_for_thread_registration(int num_expected) //確保所有執行緒都在準備中 int tpool_add_work(void *pool, void(*routine)(void *), void *arg) //配合 `dispatch` 加入 task 到 work queue 中 static void *tpool_thread(void *arg) //給 worker task,沒有用到 mutex_lock static tpool_work_t *get_work_concurrently(thread_t *thread) //利用 CAS 讓每個執行緒拿到自己的 task,確保從 work queue 出來時,遞減 `out` 的變數同步 void tpool_destroy(void *pool, int finish); //判斷所有的 queue 是不是空的,確保所有 worker 完成工作 ``` :::info **CAS** 以具有 atomic 特性的計算操作來達成。 ```clike= bool CAS(T* addr, T exp, T val) { if (*addr == exp) { *addr = val; return true; } return false; } ``` ::: >[github source code](https://github.com/xhjcehust/LFTPool) #### 實做測試 ```shell $ git clone https://github.com/xhjcehust/LFTPool $ cd LFTPool/ $ make $ ./testtpool ``` ## 函式介紹 ### MMAP [SYNOPSIS]((http://man7.org/linux/man-pages/man2/mmap.2.html)) ```clike= #include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); ``` #### mmap mmap() 將檔案映射到一個 virtual memory address 上面,藉由直接對 memory address 修改的方式不用經過buffer,減少資料load到緩衝區的overhead,快速的記憶體存取可以提高對檔案 IO 的效率 :::info **Blocking** VS **Non-blocking I/O** * Blocking I/O 允許 sleep/awaken 動作的 process,基本動作: * 當 process 進行 read 動作但是資料尚未就緒時,process 必須被 block 住 (sleep),而當有資料進來時就需要立即被喚醒 (awaken),就算資料仍不完整也是 * 當 process 進行 write 動作但是 buffer 沒有空間時,process 也必須被 block 住,而且必須被放到跟 read 動作不同的 wait queue 中去等待 wake-up (buffer 騰出空間寫入) * Non-blocking I/O * 讀不到資料或寫不進 buffer 時,他會送出 retry 的動作,再試一次 >>[參考觀念](https://hackmd.io/AwJgzA7ARgHMCsBaEJjEQFgMYE4uJgFMcA2RXYEiYYmAMxgiA===?both) ::: >[記憶體映射函數 mmap 的使用方法](http://welkinchen.pixnet.net/blog/post/41312211) * `addr` : 要 map 到的記憶體位置 * 如果 addr 為 NULL,kernel 將在建立的 mapping 地址中自行選擇,這是建立新的 mapping 最簡單的方法。 * 如果 addr 不是NULL,mapping 會在附近的 page boundary 建立,並將新 mapping 地址傳回。 * `length`: 要 mapping 的長度,它會從 fd + offset 的位置開始 mapping 檔案。 * `prot`: 要保護的記憶體保護 mapping **不能跟 open mode 衝突** * PROT_EXEC 可能執行 * PROT_READ 可能讀取 * PROT_WRITE 可能寫入 * PROT_NONE 可能不能存取 * `flags`: 選擇 map file 能不能被其他程式看到 * MAP_SHARED 分享此 mapping * MAP_PRIVATE 建立一個私有 copy-on-write mapping * `fd`: 用系統函式 open 打開的檔案位置,open mode 可以選擇檔案的讀寫屬性,不能跟 prot 有衝突。 * `offset`: 從檔案的哪裡開始 mapping,offset 必須是 page size 的倍數,可以用 sysconf(_SC_PAGE_SIZE) 取得。 #### munmap munmap() 用來取消參數 start 所指的映射記憶體起始地址,參數 length 則是欲取消的記憶體大小。當行程結束或利用 exec 相關函數來執行其他程式時,映射記憶體會自動解除,但關閉對應的文件描述詞時不會解除映射。 >[C語言munmap()函數:解除内存映射](http://c.biancheng.net/cpp/html/139.html) ### dprintf ## Refactoring * 定義 「在不改變軟體外部行為的前提下,改變其內部結構,使其更容易理解且易於修改」。也就是說,在對外的介面上沒有做改變、介面背後的對應行為也沒有改變的情況下,基於可讀性,以及日後更便於修改的目的之下,來改寫內部的程式碼實作。 * 該執行重構的時機 * 增加新功能 * 修正錯誤 * 程式碼審查 > [Refactoring - Improving the Design of Existing Code](https://www.csie.ntu.edu.tw/~r95004/Refactoring_improving_the_design_of_existing_code.pdf) > [什麼是Refactoring](http://teddy-chen-tw.blogspot.tw/2014/04/refactoring.html) > [對於重構的兩則常見誤解](http://www.ithome.com.tw/node/79813) --- ## 程式碼解析 >>[參考](https://hackmd.io/IwNgZgxgLCAMDsBaMUCmAjRUCGBOEi6AHLGIuBAKzoBM6AJversEA===?both) >>參考 refactor 部分 ### debug.h * 提供 macro `dprintf`,與 `printf` 的用法一樣,只是會在前面增加 `DEBUG:` 的字樣,透過定義 `DEBUG` macro 啟用。 * 與 c library 提供的 `dprintf` 功能不一樣,c library 的為將輸出寫入指定的 file descriptor ### file_align.c * 目的:將檔案中每一行的字串長度貼到指定長度 ### main.c 1. 將檔案的內容映射到 memory 上,讓每個 thread 可以同時存取。 * `mmap`:將指定檔案的資料映射到 process 的 virtual memory space 上,可以防止 blocking I/O 的發生。 * `munmap` :刪除配給的區塊,在程式結束時,這個區塊會自動被 delete 掉。 * 關閉對應的 file descriptor 不會刪除配給的記憶體 2. 建立足夠的空 entry 給所有 thread 使用,避免因為 `malloc` 造成 waiting。 3. 準備每個 thread 所需要的資訊 * 讀取資料區間的開頭與結尾 * 寫入資料區間的開頭與結尾 5. 創造 thread,開始讓各自的 thread 工作 6. 等待所有 thread 完成之後將每一段 thread 所屬的 entry list 組合起來 ## 程式實驗 ### original code ```clike= Performance counter stats for './phonebook_opt' (100 runs): 826,235 cache-misses # 42.230 % of all cache refs 1,986,333 cache-references 231,999,113 instructions # 1.00 insns per cycle 224,334,273 cycles 0.096829776 seconds time elapsed ( +- 1.72% ) ``` ![](https://i.imgur.com/My6mtqh.png) ### Refactor - 解決 warning 將 `phonebook_opt.h` 中的 `static double diff_in_second` 加入 `#ifdef DEBUG` & `#endif` 確認是否開啟 DEBUG 模式。 如果要使用 DEBUG 需要使用 `$ make DEBUG=1` ```clike= warning: variable ‘cpu_time’ set but not used [-Wunused-but-set-variable] double cpu_time; ``` ```clike= #ifdef DEBUG static double diff_in_second(struct timespec t1, struct timespec t2); #endif ``` - 將 `main.c` 中 opt 版本需引入的標頭檔集中在 `main.c` 的前面方便閱讀 - 更改 `main.c` 中的 for loop 主要修改 1. 將 i = 0 的情況移出 for loop 減少 branch 的數量 2. 更正程式錯誤 - 原本為連接 `pHead->pNext`,但應連接 `pHead` 才對 ```clike= pHead = pHead->pNext; for (int i = 0; i < THREAD_NUM; i++) { if (i == 0) { pHead = app[i]->pHead->pNext; dprintf("Connect %d head string %s %p\n", i, app[i]->pHead->pNext->lastName, app[i]->ptr); } else { etmp->pNext = app[i]->pHead->pNext; dprintf("Connect %d head string %s %p\n", i, app[i]->pHead->pNext->lastName, app[i]->ptr); } etmp = app[i]->pLast; dprintf("Connect %d tail string %s %p\n", i, app[i]->pLast->lastName, app[i]->ptr); dprintf("round %d\n", i); } ``` ```clike= pHead = app[0]->pHead; dprintf("Connect %d head string %s %p\n", 0, app[0]->pHead->pNext->lastName, app[0]->ptr);![](https://i.imgur.com/yPhHzO0.png) etmp = app[0]->pLast; for (int i = 1; i < THREAD_NUM; i++) { etmp->pNext = app[i]->pHead; dprintf("Connect %d head string %s %p\n", i, app[i]->pHead->pNext->lastName, app[i]->ptr); etmp = app[i]->pLast; dprintf("Connect %d tail string %s %p\n", i, app[i]->pLast->lastName, app[i]->ptr); dprintf("round %d\n", i); } ``` - 更改 `phonebook_opt.c` 中變數名稱 - DEBUG: find string - DEBUG: thread i append string - 出現很多 DEBUG 和 匯流排遺失 >>[參考程式](https://hackmd.io/s/Bk-rtQGR#code-refactoring) ```clike= Performance counter stats for './phonebook_opt' (100 runs): 789,020 cache-misses # 37.669 % of all cache refs 2,168,562 cache-references 230,179,204 instructions # 1.07 insns per cycle 231,563,707 cycles 0.096281171 seconds time elapsed ( +- 1.38% ) ``` ![](https://i.imgur.com/yXf4RiA.png) ### Thread Pool >[github source code](https://github.com/mbrossard/threadpool) ```clike= threadpool_t *threadpool_create(int thread_count, int queue_size, int flags); int threadpool_add(threadpool_t *pool, void (*routine)(void *), void *arg, int flags); int threadpool_destroy(threadpool_t *pool, int flags); ``` ```clike= Performance counter stats for './phonebook_opt' (100 runs): 753,916 cache-misses # 35.587 % of all cache refs 2,129,353 cache-references 238,219,831 instructions # 1.03 insns per cycle 223,924,023 cycles 0.095207583 seconds time elapsed ( +- 1.48% ) ``` ![](https://i.imgur.com/poV0vGv.png) - 執行結果不明顯 #### 各 work queue 和 worker thread 的比較結果圖 ### Lock-free Thread Pool >[github source code](https://github.com/xhjcehust/LFTPool) ## --- <style> h2.part {color: red;} h3.part {color: green;} h4.part {color: blue;} h5.part {color: black;} </style>