# 七周七線程模型練習筆記
## 第一章 並行與並發
* 並發是同一時間應對(dealing with)多件事情的能力
* 並行是同一時間執行(doing)多件事情的能力
舉例如下:
* 一個服務生一次只能幫一個客人寫點菜單 -> 非並發
* 加只有一個廚師一次只能做一道菜 -> 不並發且不並行
* 加同時有多個廚師分工做菜 -> 非並發但並行
* 一個服務生可以同時接受多個客人的點菜單 -> 並發
* 加只有一個廚師一次只能做一道菜 -> 並發但不並行
* 加同時有多個廚師分工做菜 -> 並發且並行
此外並發可能導致不確定的結果(因為時序的錯置)
但並行不會導致不確定的結果
位元級並行
e.g., 32 bit
指令級並行
e.g., Instruction pipeline
數據級並行
SIMD
任務級並行
Multi-core processor
## 第二章 線程與鎖
### 案例學習
特色:簡單粗暴
> 註:在產品上應該操作像是 std::thread 之類已經包裝過驗證過的線程服務,不應該直接使用 pthread 之類底層的服務。
注意以下特性
1. 共享內存。
2. 亂序執行
3. 線程 A 對內存的修改對於線程 B 可能是不可見的。
* (未同步前不保證線程間內存可見性)
4. 效能與同步頻率成反比
不同的上鎖方式也會導致效能的大幅區別
哲學家問題
簡單的 pseudo code
```=c
left.lock()
right.lock()
eat()
left.unlock()
right.unlock()
```
以上寫法可能造成 deadlock
can we do better?
```=c
left.lock() or right.lock()
another.lock()
eat()
left.unlock()
right.unlock()
```
但這樣的效率可能不好,有可能只有一個人在 eat() 的時候其他人都拿著一枝筷子在發呆
can we do better?
```=c
table.lock()
try {
while(left == eat || right == eat) {
table.await()
}
left = eat;
right = eat;
} finally{ table.unlock() }
```
await 時會暫時釋放鎖,直到有 signal 喚醒,這樣可以保證只要左右都沒在 eat 的情況下該哲學家必可以 eat
後面講 thread pool
用生產者消費者題目,計算詞彙出現頻率
1. 一個生產者分配成多個生產者 (解析的效能提昇)
2. 多個消費者用鎖來寫入單一 map (大量的同步導致效能下降)
3. 多個消費者使用 lock striping 的單一 map (統計的效能提昇)
4. 多個消費者各自使用 map 並在最後合併 (統計的效能再提昇)
### 總結
#### 優點
* 應用廣泛
* 正確使用時效率高
* 各語言支援度高
#### 缺點
* 並非直接支援並行
* 無助於分佈式的模型
* 可維護性低
* 難以除錯
## 第三章 函數式編程
> Clojure 我一下子真的看不是很懂,效率起見,先跳過
## 第四章 分離標誌與狀態
> 本章為上一章的延伸,一樣跳過
## 第五章 Actor
> 有點類似 message queue + thread pool + 封裝的概念la (對 trevi 的人來說就是 fiber 啦)
這個函數式語言我比較看得懂了,以下實驗紀錄
對函數式語言來說函數是第一類的物件-可以綁定到變數、作為函數參數、與數據沒有分別
e.g.,
```=clojure
> double = &(&1*2)
> twice = fn(fun) -> fn(x) -> fun.(fun.(x)) end end
> twice.(double).(3)
12
```
以下是轉換為 C++ 的對應寫法
```=c
#include <iostream>
#include <functional>
using namespace std;
int mut(int x) {
return 2*x;
}
int twice(function<int(int)> func,int a) {
return func(func(a));
}
int main() {
using namespace std::placeholders;
//auto funcObj = &twice;
cout << mut(2) <<endl;
cout << twice(bind(&mut,_1),2) <<endl;
// print 8
return 0;
}
```
但這個綁定 int 了,挑戰一下能不能用 template 解決
```=c
template <class T> T twice(std::function<T(T)> func, T b) {
return func(func(b));
}
```
發現好像不行,因為 std::function 本身其實也是 template ,這樣我等於在 template 定義了用 template 當參數,這樣編譯器似乎沒辦法生成對應的函式。
> 現在才驚覺 `#include <functional>` 裡面的功能就是在模擬 functional programming,也所以 lambda 的宣告方式回傳的是 std::function 的 obj
Actor 之間互相用非同步調用的方式通訊,通訊時傳入自己的 pid ,這樣對方就知道 response 的時候要打到哪個 Actor 去。
也可由此得知 Actor 之間的連接是雙向的
Actor 內部自己維護一個 message queue ,用串行的方式處理 queue 內的訊息,再用異步的方式回應到對方的 message queue
由於每個 Actor 各自只負責一個模塊,通過訊息溝通,所以也沒有內存同步的問題。
### 錯誤處理
如果 Actor 崩潰導致預期的回應沒有送達對方的信箱怎麼辦
此語言定義兩個原則
1. 如果沒異常,回應一定會送到
2. 如果有異常,異常訊息一定會被送到
由上往下讓每個 Actor 都有對應的 daemon 管理,如果崩潰則通知其使用者。
daemon 本身崩潰的話上層的管理者也會發現,這樣除了 root 以外的崩潰都會收到異常訊息。
這是 error-Kernel mode
哲學 -> 我們不能阻止所有崩潰 -> 任其崩潰 -> 在管理下的崩潰即可
> 以下是自我思考
如果要實現一個有優先度的 message queue ,可以嘗試按照優先度做 hash ,而不是全部排序在同一個 queue 內,效率會好很多。(建立在優先度沒有分太多的情況下)
>
> 如果優先度真的分很多的話可以考慮改用 map (RB tree) ,因為是排序過的,所以只要從頭開始掃就好,內建滿足按照優先度排的需求。
一樣是統計詞彙頻率
拆成 解析器 -> 計數器 -> 累加器
解析器維護一個 message queue ,自己把本地的檔案加到 message queue 內並執行,解析完成之後把文本傳給後面的計數器,並把這個檔案重新加到 message queue 內(這樣如果這次通訊是失敗的,那後面就會自動重試)
只有在累加器確認該文本已經計數並累加完畢之後,再通知解析器把該文本從 message queue 內刪掉。
優點:分佈式的計數器可以崩潰沒關係,只要 daemon 能正常的重啟它就以不斷繼續運行。
缺點:累加器跟解析器的狀態必須同步,在沒有這部份處理的情況下這兩個是不能崩潰的。
## 第六章 CSP
> 怎麼又是 Clojure =_=,就不能是 go 嗎QAQ
CSP 相較於 Actor 的差別是 CSP 注重在通道本身上的操作上, Channel 本身是可以轉移的,變成把 chaneel 本身也當成第一類的物件。
channel 可以開啟或關閉,對於一個關閉的 channel 寫入會直接被丟棄,只能取出內部的值。
取完了的話會回傳 nil ,故將 nil 塞進 channel 是非法的。
Channel 本身是一個 thread safe 的 Queue,並且分為幾種設計。
1. no buffer
2. slidding buffer
3. droping buffer
對於 no buffer 的寫入會 blocking , droping buffer 的話則是丟掉新的, slidding buffer 則是會移除最舊的。
:::info
為什麼沒有容量自動擴展的 channel ?
因為就算容量會自動擴展,但實際上還是會有其上限,而且其上限會成為 undefine behavior。
使用者應該一開始就想好緩存塞滿了該怎麼辦,避免製造出隱藏的 bug 。
:::
### thread pool 的缺陷解決方案
當涉及 blocking 的操作時 thread pool 反而可能造成整體系統的崩潰。
一旦 thread 出現 block 將有可能無窮的被佔用。
其一的作法是改為事件驅動的寫法,在這裡 go module 提供了一個狀態機形式的方案。(控制反轉)
它以對 channel 的操作作為分界點,將程式碼的行為切割為一種狀態,只要保存下狀態,這個狀態就可以在任何的 thread 上還原,來解讀一下以下代碼。
```=Clojure
(go
(let [x (<! ch)
y (<! ch)]
(println "sum" (+ x y))))
```
首先會停在 `x (<! ch)` 的部份,直到 channel 內有值可以讀取為止。
注意這個停止並不是 blocking ,而是保存下當前狀態然後直接將這個 thread 讓出給別人執行。
當有值可以讀取之後將狀態機還原在任何一個閒置的 thread 上,然後讀取,然後再停止直到有值可以讀取為止。
由於這樣的作法不會佔用線程資源,所以成本相對低很多,而能夠不在意資源不斷新增並發的任務有相當革命性的意義。
所以各個 module 的傳遞不是靠 lock 之類的而是靠 channel 的寫入跟讀取,一樣以統計詞彙為例:
輪詢 api * n -> 解析連結 * n -> 紀錄已統計連結 -> 統計詞數 * n -> 合併結果
中間解析完連結之後就可以直接詢問 `紀錄已統計連結` 有沒有存在該連結,沒有存在則繼續往下。
合併完結果在嘗試把以統計連結更新(不過這樣的寫法有點脆弱,已統計連結會不斷增加直到爆掉為止)
### 優點
1. 閒置的 module 不會佔用線程。
2. 各 module 可以透過 thread safe 的 channel 交互,比起要 link 才能交互的 actor 更靈活。
3. 一個 module 甚至可以監聽兩個以上的 channel 。
4. 透過 channel 來取代 callback 可以讓程式更加清晰高效。
5. 透過控制反轉的原理,甚至可以在瀏覽器上用 Js 模擬並發的行為。
> 註:瀏覽器上的 Js 是單線程的,不支援並行,但這不影響並發。且通過 Web Worker 現代的瀏覽器已經某種程度上支援多線程了。
### 缺點
1. 由於各個 module 並不像 Actor 真的建立了雙向的連接,所以不確定性是存在的。
2. 此外 channel 用的不好的話 deadlock 也有可能發生。
3. SCP 相較於 Actor 還缺少全套 daemon 的機制,所以對分佈式跟容錯性的支持較差。
本書總結如下: Actor 注重容錯性及分佈式的架構, CSP 注重效能以及整體流程的簡潔,兩者依使用情境而定。
:::info
有一部份我覺得還是按照寫程式的方式而定吧,在前兩章的案例中 Actor 也沒辦法做到完全 stateless, CSP 也有做到部份的 stateless ,這部份就可以拆成分佈式,所以總歸還是看寫的人怎麼寫。
:::
## 第七章 SIMD
並行的進行獨立運算
此處以 OpenCL 為例
例如 A 陣列 + B 陣列
`result[i] = A[i] + B[i]` 本身是獨立運算
該運算本身被稱為 kernel ,屬於一個物件
需要先把一個 test.cl 讀入,`clBuildProgram` 編成物件
然後傳入運算本身,運算次數,緩存大小之類的給 command queue
由於 buffer 都是 Clike 的(支援 vla),所以取得不同平台預設長度的行為變成透過不輸入參數呼叫同一個 function 來取得該 function 的該平台參數。
此外 OpenCL 不支援一維以上的陣列,要自己計算偏移來模擬二維行為。
此外 OpenCL 也支援 CPU 加速,內部有整合 SSE 或是 AVX 之類的實作
支援 barrier 的使用,在類似折疊運算的技巧上很常用到。
## 第八章 Lambda 架構
Functional programming + MapReduce + SIMD
Hadoop 對文件切分交給 Mapper
Mapper 負責將輸入對應到 key
Reducer 負責將 pair 轉換成最終輸出
Hadoop 保證 consistent hash ,相同的 pair 一定會被交給相同的 Reducer 處理
這是一個比較大的架構,需要在 Aws 上實作,故此處只紀錄大概方向
幾個要點
1. 讓所有原始數據唯讀
* 這樣可以避免 race condition,有寫入需求時就另外寫一筆附加資料進去
* 需要統計時再統計所有的原始資料+附加資料
2. 寫入讀取分離(批處理層)
* 將寫入區和讀取區獨立開來,讀取區為固定的 const data,每次操作都只是添加在寫入層之上,過一陣子之後在透過批處理將寫入層的資料一次更新到讀取層,由於寫入層的都只是獨立操作,所以可以用並行運算快速處理完成。
3. 異步調用
* 異步的使用 message 處理 client 端的事件
4. 加速層
* 寫入讀取分離的一種解決方案,針對秒級應用維護一個加速層,加速層在批處理區間維護最新的狀態,並且在批處理完畢後同步並切換,加速層採取無限增量的策略,每次操作都增加在源數據之上,直到批處理完成之後加速層馬上廢棄所有當前狀態,以下舉例
批處理層 A = 10
加速層 A+2, A+3, A+9, A-3, A+1, A+5...
不斷疊加上去,取用的時候就取用批處理 + 加速全部的運算結果(可並行,因為每次都是獨立操作)來使用
一旦批處理層更新完畢,就會變成這樣
批處理層 A = 27
加速層 null
按照批處理的速度不同,可能會需要 N 個加速層交替使用。
### 優點
1. 架構級的解決方案,有處理大數據的能力
2. 非常適合產生報表,統計分析
3. 在分散式的架構下具有高容錯性
### 缺點
1. 成本高,硬體軟體的成本都高
2. 架構級的解決方案,對於較為細小的問題沒有幫助