# 2020_OS_Fall_HW2: ETL process ###### tags: `os-2020` ## 作業說明 - [link](https://hackmd.io/@dsclab/rktgrMOUD) ## 作業程式碼 - [GitHub](https://github.com/Jonec76/os-hw2) ## 學生資料 學號:P76091129 姓名:王子源 系級:資工所碩一 ## 說明文件 ### 開發環境: - OS: Ubuntu 16.04.1 - CPU: Intel® Core™ i5-8500 CPU @ 3.00GHz × 6 - Memory: 8GB - Programming Language(version): c++ ### 執⾏時間 input size:1004 Mb 秒數 :77.2413s thread 數⽬: 6 lines_per_thread : 50 ### 程式開發與使用說明: - 程式開發概念 此次作業是希望能夠透過多執⾏緒,⽬的希望將 csv 檔案轉成 json 檔案。在讀檔時,能夠⾃⾏選定要多少條 thread 同時執⾏,每條 thread 會負責處理轉換特定幾⾏字串後,再換下⼀條 thread。 在寫完後才看到作業要求裡⾯提到 Stage 最少會包含讀檔、資料處理、輸出三個階段。⾃⼰這邊的處理並不是如作業要求的⽅式,⽽是這三個階段不斷重複性地做。會這樣寫是想說試著減少執⾏時間,不希望將⼤筆資料載入後⼜從頭做資料處理,做完⼜做⼀次輸出結果。 - 程式架構 ![](https://i.imgur.com/VAqnpkp.png) 在寫 HW1、HW2 的作業都有⼀個感想,就是必須去處理⼀些特殊的情況,尤其是在讀檔最後⼀ run 時。以這次作業來講,我希望每條 thread 可以負責轉換特定⾏數(不同⾏數會影響到執⾏時間,在下⾯效能分析有另外討論),並且在執⾏完所有 thread 後,依序等待每個 thread 的結果,此時就會依序將結果輸出。會在⼀開始建立需要⽤到的 thread 以及每個 thread 需要的參數 ```c=1 typedef struct{ char** line; char* result; size_t limit_set; } Param; Param paramm_obj[NUM_THREADS]; pthread_t thread[NUM_THREADS]; ``` 接著使⽤ [pthread api](https://man7.org/linux/man-pages/man7/pthreads.7.html) 產⽣新的 thread ```c=1 pthread_create(&thread[t], &attr, threaed_task, (void*) &param_obj[t]); ``` 這裡有兩個部分需要注意 1. 若是要傳參數給 thread,只能透過(void *)的⽅式,所以若是要傳多個參數的話,需要先透過 struct 定義⼀個新的物件 2. 這個是開發過程碰到的盲點,⼀開始並不是使⽤陣列去存取變數,⽽是多個 thread 共⽤⼀個變數,為何⼀定要使⽤陣列分開呢?這是因為如果產⽣⼀個 thread A 並且將參數 assign 之後,thread A 並沒有立刻執⾏,⽽是改變了參數後⼜ assign 給 threadB,此時改過的參數會影響到 thread A(因為是 call-by-reference),進⽽導致資料錯誤。 透過上述 API 產出新的 thread 之後,就要去執⾏每個 thread 需要做的 task,定義如下 ```c=1 void str_to_json(char** result, char* line){ size_t ln = strlen(line) - 1; if (*line && line[ln] == '\n') line[ln] = '\0'; int index = 1; char* token = strtok(line, "|"); while (token != NULL) { if(index != 20){ char tmp[64]; sprintf(tmp, "\"col_%d\":\"%s\",\n", index++, token); strcat(*result, tmp); } else{ char tmp[64]; sprintf(tmp, "\"col_%d\":\"%s\"\n", index++, token); strcat(*result, tmp); } token = strtok(NULL, "|"); } assert(index == 21); } void *threaded_task(void *param) { mu.lock(); Param* p = (Param*) param; // 20 lines * 64 = 1 set p->result = (char*) malloc(p->limit_set * 20 * 64); for(size_t i=0;i<p->limit_set;i++){ strcat(p->result, "{\n"); str_to_json(&p->result, p->line[i]); strcat(p->result, "},"); } mu.unlock(); pthread_exit((void *)param); } ``` 因為希望當⼀個 thread 在處理轉換的過程時,不希望被其他的 thread ⼲擾,所以這部分需要使⽤ mutex 來產⽣⼀個 critical section,⽽ str_to_json 就只是基本字串處理。 當產的 thread 數量已經達到限制的數字時,就要將結果輸出成檔案。因為 multi-thread 的特性就是 non-synchronize,若不額外做處理的話,就會依據哪條 thread 做完就輸出哪條thread,這會導致輸出的結果沒有順序性。所以需要透過 pthread_join 的⽅式,讓所有的 thread 有順序的結束並將結果輸出,程式碼如下 ```c=1 for (t = 0; t < thread_in_use; t++) { rc = pthread_join(thread[t], &status); if (rc) { printf("ERROR; return code from pthread_join() is %d\n", rc); exit(-1); } fprintf(out_fp, "%s", ((Param*)status)->result); for(size_t j=0;j<param_obj[t].limit_set;j++){ free(param_obj[t].line[j]); } } ``` 先做的 thread 先輸出,符合 FIFO 原則的話,這樣的結果就會是有順序性的了。重複以上的步驟直到結束時,基本上就能處理 99% 左右的資料了,剩下的就是⼀些特殊的case 需要額外做處理。以我的程式碼設計,唯有 thread 產出達到指定的數⽬時,才會執⾏ pthread_join 並且輸出,那假如在最後⼀回合沒有達到指定數⽬呢?這樣會有⼀些thread 被產⽣,但沒有將他們回收,所以需要在讀完檔離開 while 迴圈後,加了⼀段 ```c=1 if(thread_in_use != NUM_THREADS) ``` 來進⾏檢查。 另外,還需要檢查另⼀個 case,每條 thread 都有負責的 lines_per_thread 列數需要處理,如果最後⼀回合沒有達成的話,那也需要在 while 迴圈後另外處理。 ```c=1 if(!new_obj){ param_obj[t].limit_set = limit_ctr; rc = pthread_create(&thread[t], &attr, threaded_task, (void *)&param_obj[t]); if (rc) { printf("ERROR: return code from pthread_create() is %d\n", rc); exit(-1); } rc = pthread_join(thread[t], &status); if (rc) { printf("ERROR; return code from pthread_join() is %d\n", rc); exit(-1); } fprintf(out_fp, "%s", ((Param*)status)->result); } ``` ## 效能分析 - 測試資料 這邊測試使用了 thread 數目為 1, 5, 10,另外還有調整了 `lines_per_thread` 的大小為 1, 5, 10, 50, 100, 500,`lines_per_thread` 大小是控制每條 thread 應該處理多少條 line,為求測試速度效率,使用了約 52M 小筆資料當實驗 input file。 - 量測結果 ![](https://i.imgur.com/heyiSmQ.png) 上圖使用 `gnuplot` 量測工具幫忙繪圖,並且以測試時間當作 y 軸來探討,而做完實驗後覺得可以整理出以下 2 件觀察 1. 越多 thread 越好嗎? 我想以時間的結果來看則是未必,可以看出其實最省時的是只有 1 條 thread 的情況去執行此程式,其中可能會有很多的因素,而自己認為就是因為越多的 thread 會導致 OS 管理上的負擔,使得越多 thread 反而時間花越久 2. `lines_per_thread` 越大越好嗎? 這個 lines_per_thread 是指 thread 負責要處理多少列的資料,從圖中可以看出個有趣的部分,是當 lines_per_thread 在大約 50 時,會是時間花最少的。 如果 lines_per_thread 越少,表示每條 thread 負責少量的資料,那將會重複更多回合在上面的步驟,產生 thread 的次數也將會變多次,所花時間越多。而 lines_per_thread 越大表示每個 thread 要負責的資料量越多,我在猜想會導致時間越多的原因,有可能是因為在 `pthread_join` 時要等待其他 thread 做完的時間變久了,而導致要花的時間也變多。 此外,這次還有使用一個第三方程式 [KSar](https://www.thomas-krenn.com/en/wiki/Linux_Performance_Analysis_using_kSar) 來協助更詳細的監控作業系統,以下附上數據結果 - CPU usage 以下這張圖是多筆測資綜合起來一起看的,圖中每個高峰的順序分別是 thread 數目為 1, 5, 10,另外還有調整了 `lines_per_thread` 的大小為 1, 5, 10, 50, 100, 500(每個 thread 由左到右) ![](https://i.imgur.com/RuQsKgi.png) 從結果可以觀察出下列幾件事情: 1. 多執行緒的 CPU usage 普遍比 1 條 thread 的使用量還高 2. 當 lines_per_thread 提高時,同時也降低了不斷重複產生新的 thread 的次數,所以紅色的部分(%sys)看起來有明顯變少 此部分也可以從 I/O 的 TPS (the number of transfers per second) 觀察出相同的結果 ![](https://i.imgur.com/x14wLcS.png) - process 下圖指出了 Task creation 以及 Context switch ![](https://i.imgur.com/keCD9HD.png) 可以看到當 lines_per_thread 變大時,兩個觀察項目的縮減相當明顯,也可以看出程式在 lines_per_thread 很小時,會發生多次的 context switch 以及 task creation ,所以才會使得上面量測結果的圖,在起初的 lines_per_thread 時間會花那麼多的原因。 另外,也蠻好奇不確定是因為自己電腦是 6 核心的,所以在 10 threads 表現上並沒有比 5 threads 還高,還是其他原因沒有發現的。 更多未提到的分析將會在 「結果分析.pdf」呈現,裡頭尚包含了每個 CPU 的使用狀況、Kmem 以及 Swap 使用情形。