---
tags: ncku-course
---
# 2016q3 Homework2 (phonebook-concurrent)
contributed by <`0140454`>
## 資料閱讀
### 軟體開發現況
#### [The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software](http://www.gotw.ca/publications/concurrency-ddj.htm)
* 過去
過去30年內,CPU Designer 從三大領域中提升執行效能,分別是「Clock Speed」、「Execution Optimization」以及「Cache」。
* 現在
但這幾年來,CPU Designer 為了榨乾 CPU 的效能,有可能利用激進的 Instruction Reordering,進而讓程式執行結果與預期的有所不同。
而在短期內,CPU 的效能增長來源將改為「Hyper-threading」、「Multi-core」以及「Cache」。
* 未來
因為在硬體設計上遇到了一些瓶頸,而 Cache 的部份越來越大,所以未來會朝向 Concurrency 的方向發展。
Concurrency 的優點是各執行緒之間的控制流程相互獨立,而缺點主要是在透顧 lock 來同步化時所需要的成本極高。
#### [An Introduction to Lock-Free Programming](http://preshing.com/20120612/an-introduction-to-lock-free-programming/)
* What is lock-free?
簡易的分辨流程:
![](https://i.imgur.com/FwuEx8a.png)
lock-free 中的 `lock` 其實不是指 mutex 等等,而是指 ==不會被鎖定==。
* Lock-free Programming Techniques
![](https://i.imgur.com/NKTvjj4.png)
* **Atomic Operations**
所謂的 atomic operation 即該操作在系統中是不可再繼續被分割的操作。
目前各平台都有提供相關的 API,如 Windows 下的 `_InterlockedIncrement`、iOS 下的 `OSAtomicAdd32` 以及 C\++11 中的 `std::atomic<int>::fetch_add`。
但值得一提的是,C\++11 的 atomic operation 並沒有保證他是 lock-free 的,所以必須透過 `std::atomic<>::is_lock_free` 來確認。
* Compare-And-Swap
最被廣泛討論的 atomic operation 方法。過程就是「先從記憶體中讀取資料,若與預期的相同才將新的值寫入」。但必須要注意到 [ABA Problem](https://zh.wikipedia.org/wiki/%E6%AF%94%E8%BE%83%E5%B9%B6%E4%BA%A4%E6%8D%A2#ABA.E9.97.AE.E9.A2.98)。
* Sequential Consistency
在所有執行緒中,對記憶體的操作會依尋著特定的順序進行,而且也會與程式碼中的順序一樣。
#### [Acquire and Release Semantics](http://preshing.com/20120913/acquire-and-release-semantics/) & [Weak vs. Strong Memory Models](http://preshing.com/20120930/weak-vs-strong-memory-models/)
透過 Acquire and Release Semantics 來限制優化,讓記憶體相關操作必須在特定語句前後完成。
```
+----------------------------------------+
| Acquire Semantics |
+----------------------------------------+ <<= 所有記憶體操作要在此線之下執行
| |
| .... |
| |
+----------------------------------------+ <<= 所有記憶體操作要在此線之前執行
| Release Semantics |
+----------------------------------------+
```
* Memory Barrier
透過一些指令防止 CPU 對一些記憶體操作進行重排。共分4種 Barrier。
* LoadLoad
* StoreStore
* LoadStore
* StoreLoad
* Weak vs. Strong Memory Model
所謂的 Weak 就是指有很大的機率會將 load/store 指令進行重排,而 Strong 則相反。
---
### Concurrency 相關
#### Concurrency vs. Parallelism
Concurrency 指的是兩個以上的工作 **相互交錯** 執行。
![](https://i.imgur.com/aBbB2L4.png)
而 Parallelism 則是指兩個以上的工作 **同時** 執行。
![](https://i.imgur.com/zFXeQ5y.png)
#### Concurrency 系列文章
因為編譯器會最佳化程式碼的執行順序、處理器會亂序執行、硬體又可能有 Cache coherent 的問題,所以程式實際的執行過程和我們寫的不一定相同。
* Sequenced-Before
在同 thread 下,對求值順序關係的描述。
以 A is sequenced-before B 為例,也就是 A 會先求值完後才開始進行 B 的求值。
所以當有 A is sequenced-before B 且 B is sequenced-before A 的情況發生時,表示我們無法確定他的求值順序,抑或是他們是同時求值。
* Happens-Before
前一個操作的效果在後一個操作執行之前必須要可見。
happens-before 所關注的是 ==在執行前可見==,不同於 happening-before 是 ==發生的順序==。
而在同一個 thread 中的 happens-before 就是上面提到的 sequenced-before。
* Synchronized-With
發生在兩個不同 thread 間的同步行為。當 A is synchronized-with B 時,代表 A 對記憶體操作的效果,對於 B 是可見的。而 A 和 B 是兩個不同的 thread 的某個操作。
換句話說,synchrozied-with 就是不同 thread 間的 happens-before。
* Sequential Consistency
正如上面提到的,系統在執行時都維持程式原有的順序。在文章中舉 Dekker's algorithm 為例子,說明其重要性。
---
#### Thread Pool
Thread 雖然可以說是一個 lightweight process,但是在 create 跟 destroy 的時候還是需要一定的成本。
所以就有了 thread pool 這個概念,thread pool 讓我們在一開始先建立 worker thread,當有工作進來的時候先丟入 work queue,然後再指派給各 worker thread。
這中間免去了頻繁的 create 與 destroy,進而使得程式的效率以及工作的分配變得更好。
* 常見的實作方式
只擁有一個 work queue,必須仰賴 mutex 等方式來確保正常的執行。
![](https://i.imgur.com/Ixmuek3.png)
* Lock-free 的實作方式
利用 mutex 來實作的 memory pool 必定會受到 mutex 的成本所影響,因此便有利用 CAS 等等的 atomic operation 來實作的版本。
作業說明中提及的 LFTPool 便是讓每個 work thread 擁有自己的 queue,當有工作進來的時候,利用 round-robin 的方式分配下去。
![](https://i.imgur.com/TerJbNU.png)
## 案例分析
### 正確性驗證
將優化版本的 linked list 印出後與原本的 words.txt 做比較,可以發現少了4個單字。
```shell=
$ colordiff -Naur dictionary/words.txt sorted.opt.words.txt
--- dictionary/words.txt 2016-10-03 10:59:13.531775949 +0800
+++ sorted.opt.words.txt 2016-10-06 02:32:27.803229037 +0800
@@ -1,7 +1,3 @@
-aaaa
-aaaaa
-aaaaaa
-aaaaaaa
aaaaaaaa
aaaaaaaah
aaaaaaauugh
```
原因在於合併 linked list 的時候,不小心把各個 linked list 的第一個 node 給跳過了。
修正方法就是將 main.c 中的
```clike=105
pHead = app[i]->pHead->pNext;
...
etmp->pNext = app[i]->pHead->pNext;
```
改成
```clike=105
pHead = app[i]->pHead;
...
etmp->pNext = app[i]->pHead;
```
### 閱讀程式碼
#### main.c
* 利用 mmap 將檔案內容 map 到 VMA 中
```clike=
char *map = mmap(NULL, fs, PROT_READ, MAP_SHARED, fd, 0);
```
* 先分配好所有 entry 的記憶體
fs 是檔案大小,因為檔案已經對齊了
所以 fs / MAX_LAST_NAME_SIZE 就等同於行數
```clike=
entry *entry_pool = (entry *) malloc(sizeof(entry) *
fs / MA X_LAST_NAME_SIZE);
```
* 設定 Concurrency Level
我們可以透過 `pthread_setconcurrency()` 來提示系統。預設而言,concurrency level 為 0,也就讓系統自行決定。
除此之外,man page 的 NOTES 也提到,當系統的 thread model 為 1:1 時,這個函數是沒有太大的意義。
```clike=
pthread_setconcurrency(THREAD_NUM + 1);
```
> 在 pthreads 的 [man page](http://man7.org/linux/man-pages/man7/pthreads.7.html) 中提到,對於 POSIX threads 的實作,Linux 不外乎分為 LinuxThreads 與 NPTL。可這兩種方式都是 1:1 的 model,所以在這邊是否有用呢? [name=吳勃興]
#### phonebook_opt.c
* 準備 thread argument
`new_append_a()` 是負責準備要傳入 append 的參數。
在命名上有點讓人看不出來,在下一個區塊 Code Refactoring 會修改一下。
```clike=
append_a *new_append_a(char *ptr, char *eptr, int tid, int ntd,
entry *start)
```
* 建立 linked list
`append()` 會將資料放入一開始所分配好的 memory pool 中。
第一個 thread 負責的行數是:1、1 + THREAD_NUM、1 + THREAD_NUM * 2、...
第二個 thread 負責的行數是:2、2 + THREAD_NUM、2 + THREAD_NUM * 2、...
剩下的以此類推。
### Code Refactoring
#### main.c
* 將 [Line 50](https://github.com/0140454/phonebook-concurrent/blob/3c3f16e7adbcb409d1a6840b67b25efc557b2937/main.c#L50) 的 include directive 移到檔案的開頭,方便閱讀。
```clike=
#ifdef OPT
#include "file.c"
#include "debug.h"
#include <fcntl.h>
#define ALIGN_FILE "align.txt"
#endif
```
* 將 [Line 104](https://github.com/0140454/phonebook-concurrent/blob/3c3f16e7adbcb409d1a6840b67b25efc557b2937/main.c#L104) 中的 `i == 0` 狀況移到 for-loop 外,減少 branch 的時間。
```clike=
pHead = app[0]->pHead;
e = app[0]->pLast;
for (int i = 1; i < THREAD_NUM; i++) {
e->pNext = app[i]->pHead;
e = app[i]->pLast;
}
```
#### phonebook_opt.[ch]
* 傳遞給 `append()` 用的結構如下,有些命名並不是那麼直觀,而且風格不太統一。
```clike=
typedef struct _append_a {
char *ptr;
char *eptr;
int tid;
int nthread;
entry *entryStart;
entry *pHead;
entry *pLast;
} append_a;
```
修改後如下
```clike=
typedef struct __APPEND_ARGUMENT {
char *pDataStart;
char *pDataEnd;
int tid;
int nThread;
entry *pListHead;
entry *pListTail;
} append_argument;
```
* 在 `append()` 中的 for-loop 有一點難閱讀,改成 while-loop 會比較好一點。
```clike=
int count = 0;
entry *new_node = app->pListHead;
char *cur_last_name = app->pDataStart;
while (cur_last_name < app->pDataEnd) {
app->pListTail = app->pListTail->pNext = new_node;
app->pListTail->lastName = cur_last_name;
app->pListTail->pNext = NULL;
cur_last_name += MAX_LAST_NAME_SIZE * app->nThread;
new_node += app->nThread;
++count;
}
```
* 在 main.c 跟 phonebook_opt.c 中都有 `diff_in_second()`,因此把他拉出來,放在 utils.c 中。
```clike=
#ifndef _UTILS_H
#define _UTILS_H
#include <time.h>
double diff_in_second(struct timespec t1, struct timespec t2);
#endif
```
* 在編譯的時候可以看到一個 warning message
```shell=
phonebook_opt.c: In function ‘append’:
phonebook_opt.c:46:12: warning: variable ‘cpu_time’ set but not used [-Wunused-but-set-variable]
double cpu_time;
^
```
原先 `cpu_time` 是為了要讓 dprintf 來輸出,但是在沒有 define DEBUG 的情況下,會使得他成為未始用的變數。
為了維持原本的設計,所以我利用 `#ifdef DEBUG` 和 `__attribute__((unused))` 來避免這個訊息。
```clike=
#ifdef DEBUG
double cpu_time;
#else
double cpu_time __attribute__((unused));
#endif
```
### Thread Pool
在原本的程式中就如同下圖一般,會等到所有 thread 都 join 完後才會繼續下去。
![](https://i.imgur.com/gxx8AiR.png)
可是這就有個問題,有些 thread 做的很快,有些 thread 做的很慢,這樣的話先完成的要在那邊呆呆的等。因此,在這邊會換成 thread pool 來實驗看看。
#### 原始版本
![](https://i.imgur.com/qyfhaHL.png)
#### Thread Pool (mutex)
![](https://i.imgur.com/2FoWb9v.png)
上圖更換至 threadpool-mbrossard 的結果。在 `append()` 的部份快了 0.00003 秒附近。
## 參考資料
* [Toward Concurrency](https://hackmd.io/s/H10MXXoT)
* [Memory Barriers Are Like Source Control Operations](http://preshing.com/20120710/memory-barriers-are-like-source-control-operations/)
* [記憶體映射函數 mmap 的使用方法](http://welkinchen.pixnet.net/blog/post/41312211-%E8%A8%98%E6%86%B6%E9%AB%94%E6%98%A0%E5%B0%84%E5%87%BD%E6%95%B8-mmap-%E7%9A%84%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95)
* [pthread_setconcurrency(3) - Linux manual page](http://man7.org/linux/man-pages/man3/pthread_getconcurrency.3.html)