做爲一個終身學習者,輸入和輸出是必不可少的。輸入多了以後,會發現不少中文文章很難讀,可能還有不少錯漏之處。不客氣地說,輸入的是垃圾,輸出的只能是垃圾。前端
曹大常常說須要多看英文資料,包括各類新出的英文書、文章等等,這從他的書單也能夠看出來。我本身的狀況是:英文資料讀的很少,英文技術書則基本就沒完整地讀過一本。以前在寫文章的過程當中,仍是看了一些英文文章,收穫很大。linux
此次嘗試讀一讀英文技術書。可是直接讀的話,常常讀完和沒讀同樣,沒有什麼感受。因而我嘗試一種邊讀書邊記讀書筆記的方式,過程當中讀到有趣的、有用的、之前不知道的地方就記下來,和你們分享。shell
這是一本 2017 年 7 月份出版的書,到今天已通過去三年了,Go 的版本也從當時的 Go 1.8,升級到了最新的 Go 1.15,變化巨大。數據庫
下面是我記的筆記:安全
併發程序常常出錯的一個緣由是人們認爲本身所寫代碼的執行順序是按書寫的順序來執行的,但在併發場景下,這顯然是有問題的。併發
Atomicity,原子性。談論原子性,必需要有一個 context。由於在一個 context 下是原子性的,但在另外一個 context 下,就可能不是原子性的了。具體的 context 多是:進程、操做系統、機器、集羣……假想個例子,在一維空間中的 X 軸上,從座標 1 到座標 3 必需要通過座標 2,這在一維空間中是絕對正確的。但做爲活在三維空間裏的人,我有不少種辦法不通過 X 軸上的座標 2 而到達座標 3。僅管個人軌跡映射到 X 軸上仍是會「通過」座標 2,這也更像一個「降維打擊」的例子。分佈式
造成死鎖的四個條件:Mutual Exclusion(併發實體任意時刻獨佔資源)、Wait For Condition(併發實體同時持有資源並都在等待其餘資源)、No Preemption(資源只能被持有它的實體釋放)、Circular Wait(循環等待,a 等 b,b 等 c,c 等 a……)。函數
活鎖是飢餓的一種,任何須要分享的資源都有可能發生飢餓,如 CPU、內存、文件句柄、數據庫鏈接等。學習
併發(Concurrency)說的是代碼,並行(Parallelism)說的是正在運行的程序。咱們沒法寫出並行的代碼,只能寫併發的代碼,而且指望它能並行執行。想象一下,咱們寫的代碼在單核 CPU 上運行,還能並行地起來嗎?測試
考察併發的代碼是不是在並行執行,咱們得看在哪個抽象的層級上看:併發原語、程序的運行時、操做系統、操做系統所在的平臺(容器、虛擬機……)、CPUs、機器、集羣……
和前面說的 Atomicity 同樣,談論 Parallelism 時,也要有一個 context。它決定是否將能將兩個操做當作並行。例如,咱們運行 2 個操做,每一個操做花費 1 秒。若是 context 是 5 秒鐘,那能夠說這兩個操做是在並行執行;但若是 context 是 1 秒鐘,那咱們認爲,這兩個操做是串行地在執行。注意,context 並不等同於時間,線程、進程、操做系統等均可以當作 context。
給併發或者說並行定義什麼樣的 context 和併發程序是否正確運行有很大關係。例如,context 是兩臺電腦,咱們分別在兩臺電腦上運行兩個計算器程序,那理論上這兩個計算器程序就是並行的,且不會相互影響。
在上面的例子裏,context 是兩臺電腦,operations 是兩個進程。很明顯,我在個人電腦上運行任何程序,都不會影響你的電腦。可是在同一臺機器上,一個進程還能保證不影響另外一個進程嗎?回答是不必定,好比讀寫同一個文件……
大部分程序的併發抽象層級是線程。Go 在抽象層級上又增長了一個 goroutine。按理說,層級層次越高,併發安全性越難保證。但實際上 goroutine 讓事情變得更容易,由於它並非在線程的抽象層級之上又加了一層,而是取代了線程。
Go channel 的設計思想來源於 Hoare 於 1978 年發表在 ACM 上的一篇關於 CSP(Communicating Sequential Processes)的論文。Go 是第一門吸取了 CSP 精華而且將其發揚光大的語言。
大多數語言使用線程+併發同步訪問控制做爲併發模型,而 Go 的併發模型由 goroutine 和 channel 組成。線程相似於 goroutine,而併發同步訪問控制則相似於 mutex。
Go 併發的理念是:簡單,儘可能使用 channel,盡情使用 goroutine。
在 linux 上,簡單測試線程切換成本:
# 在 CPU0 上執行,在兩個內核線程間發送、接收消息 taskset -c 0 perf bench sched pipe -T
由於是單核,因此在兩個線程間發送、接收消息,須要進行上下文切換。在個人乞丐版阿里雲主機上獲得結果:
# Running 'sched/pipe' benchmark: # Executed 1000000 pipe operations between two threads Total time: 69.171 [sec] 69.171280 usecs/op 14456 ops/sec
計算出大體的線程切換成本:69.171280/2 = 34.58564 us。
使用 sync.WaitGroup 時要注意,sync.Add 要在新起 goroutine 語句的外層調用,不然執行到 sync.Wait 時,可能新起的 goroutine 還沒調度到,sync.Add 天然沒執行,最終致使邏輯出錯。
mutex 是 mutual exclusion 的簡寫,翻譯一下:互相排斥。
sync.cond 有兩個比較有意思的方法:sync.Cond.Signal 和 sync.Cond.Broadcast。前者會喚醒等待時間最長的 goroutine,後者會喚醒全部等待的 goroutine。另外,要注意 sync.Cond.Wait 方法內部,隱藏了一些反作用,會先解鎖:c.L.Unlock()
,而後再加鎖:c.L.Lock()
。
查詢 Go 源碼使用了多少次 sync.Once:
grep -ir sync.Once $(go env GOROOT)/src | wc -l
channel 是粘合 goroutine 的膠水,select 則是粘合 channel 的膠水。
關於 runtime.GOMAXPROCS(n) 函數的一個可能的使用場景:代碼中可能存在 data race 的狀況,增長 n 值可讓 data race 更快地發生,從而能夠更快地調試錯誤。
爲了不 goroutine 泄露,請注意:生成子 goroutine 的父 goroutine 須要負責中止子 gotoutine,即誰建立誰銷燬。
能夠將一個「無序、耗時長」的 stage 轉成 fan-out。fan-in 是多轉一,fan-out 則是一轉多。
設計系統的時候,應該一開始就考慮 timeout 和 cancel。
分佈式系統須要支持 timeout 的幾個理由:
飽和
系統飽和時,最後到達的請求須要直接超時返回,不然可能引起雪崩;
數據過時
數據其實有必定的時間窗口,過了窗口,就是無效數據了。例如前端一個請求過來,假設用戶能夠容忍 2s,那這個窗口就是 2s,分佈式系統須要支持 2s 的超時設置,超過 2s 後數據無效;
防止死鎖
固然,觸發 timeout,有可能使死鎖變成活鎖。系統設計的目標應該是在不觸發 timeout 的狀況下不發生死鎖。
超時
超時須要取消;
用戶干預
當有用戶驅動的併發操做時,用戶可取消他發起的操做;
父節點取消
就像 context 同樣,父 context 取消了,子 context 也要跟着取消;
重複的請求
爲了獲得更快的響應,同時向幾個系統發起請求,當獲得了最快的系統響應後,取消其餘系統的請求。
能夠將多個 ratelimiter 組合在一塊兒,提供更有表達力的 ratelimiter。例如我能夠限制每秒 1 個請求,同時每分鐘限制 10 個請求。具體見第五章 Rate Limiting 小節。
Go 使用 fork-join 模型。fork 即 go func(){}(), 而 join 則通常是指 sync.WaitGroup 或 channels。
在一個函數裏(位於某個 goroutine)不斷地執行 go func(){}() 語句時,會不斷地產生相應的 goroutine,並被添加到當前 goroutine 所在的 P 上的 LRQ 中,LRQ 能夠看做是一個雙端隊列,越靠近隊列尾的 goroutine 和當前 goroutine 的空間局部性越緊密,越須要優先執行。基於這點考慮,新產生的 goroutine 並非直接放到 LRQ,而是會先放到 P 的 runnext 字段,執行完當前 goroutine 或當前 goroutine 被 park 後,首先執行的就是這個 runnext。若是以後又有新建立的 goroutine,它又會把當前掛在 runnext 上的 goroutine 頂到 LRQ 中。P 執行的時候從隊列頭的 goroutine 開始執行,而當 steal-working 發生時,也老是先從 LRQ 的頭部偷,其實就是 FIFO。
最後,全書讀起來仍是挺順暢的,所須要的知識也並無超出我現有的認知,筆記也並很少,總算是完整地讀完了第一本全英文的書吧,期待後面讀更多。