視訊筆記:理解 channels - Kavya Joshi
一、視訊資訊
1、視訊觀看地址
https://www.youtube.com/watch?v=KBZlN0izeiY
2、PPT下載地址
http://download.csdn.net/download/xunzaosiyecao/10212884
3、博文
https://about.sourcegraph.com/go/understanding-channels-kavya-joshi/
二、Go 的併發特性
- goroutines: 獨立執行每個任務,並可能並行執行
- channels: 用於 goroutines 之間的通訊、同步
1、一個簡單的事務處理的例子
對於下面這樣的非併發的程式:
func main() {
tasks := getTasks()
// 處理每個任務
for _, task := range tasks {
process(task)
}
}
將其轉換為 Go 的併發模式很容易,使用典型的 Task Queue 的模式:
func main() {
// 建立帶緩衝的 channel
ch := make(chan Task, 3)
// 執行固定數量的 workers
for i := 0; i < numWorkers; i++ {
go worker(ch)
}
// 傳送任務到 workers
hellaTasks := getTasks()
for _, task := range hellaTasks {
ch <- task
}
...
}
func worker(ch chan Task) {
for {
// 接收任務
task := <-ch
process(task)
}
}
2、channels 的特性
- goroutine-safe,多個 goroutine 可以同時訪問一個 channel。
- 可以用於在 goroutine 之間儲存和傳遞值
- 其語義是先入先出(FIFO)
- 可以導致 goroutine 的 block 和 unblock
三、解析
1、構造 channel
// 帶緩衝的 channel
ch := make(chan Task, 3)
// 無緩衝的 channel
ch := make(chan Tass)
回顧前面提到的 channel 的特性,特別是前兩個。如果忽略內建的 channel,讓你設計一個具有 goroutines-safe 並且可以用來儲存、傳遞值的東西,你會怎麼做?很多人可能覺得或許可以用一個帶鎖的佇列來做。沒錯,事實上,channel 內部就是一個帶鎖的佇列。
https://golang.org/src/runtime/chan.go
type hchan struct {
...
buf unsafe.Pointer // 指向一個環形佇列
...
sendx uint // 傳送 index
recvx uint // 接收 index
...
lock mutex // 互斥量
}
buf 的具體實現很簡單,就是一個環形佇列的實現。sendx 和 recvx 分別用來記錄傳送、接收的位置。然後用一個 lock 互斥鎖來確保無競爭冒險。
對於每一個 ch := make(chan Task, 3) 這類操作,都會在堆中,分配一個空間,建立並初始化一個 hchan 結構變數,而 ch 則是指向這個 hchan 結構的指標。
因為 ch 本身就是個指標,所以我們才可以在 goroutine 函式呼叫的時候直接將 ch 傳遞過去,而不用再 &ch 取指標了,所以所有使用同一個 ch 的 goroutine 都指向了同一個實際的記憶體空間。
2、傳送、接收
為了方便描述,我們用 G1 表示 main() 函式的 goroutine,而 G2 表示 worker 的 goroutine。
// G1
func main() {
...
for _, task := range tasks {
ch <- task
}
...
}
// G2
func worker(ch chan Task) {
for {
task :=<-ch
process(task)
}
}
2.1 簡單的傳送、接收
那麼 G1 中的 ch <- task0 具體是怎麼做的呢?
- 獲取鎖
- enqueue(task0)(這裡是記憶體複製 task0)
- 釋放鎖
這一步很簡單,接下來看 G2 的 t := <- ch 是如何讀取資料的。
- 獲取鎖
- t = dequeue()(同樣,這裡也是記憶體複製)
- 釋放鎖
這一步也非常簡單。但是我們從這個操作中可以看到,所有 goroutine 中共享的部分只有這個 hchan 的結構體,而所有通訊的資料都是記憶體複製。這遵循了 Go 併發設計中很核心的一個理念:
“Do not communicate by sharing memory;instead, share memory by communicating.”
記憶體複製指的是:
// typedmemmove copies a value of type t to dst from src.
// Must be nosplit, see #16026.
//go:nosplit
func typedmemmove(typ *_type, dst, src unsafe.Pointer) {
if typ.kind&kindNoPointers == 0 {
bulkBarrierPreWrite(uintptr(dst), uintptr(src), typ.size)
}
// There's a race here: if some other goroutine can write to
// src, it may change some pointer in src after we've
// performed the write barrier but before we perform the
// memory copy. This safe because the write performed by that
// other goroutine must also be accompanied by a write
// barrier, so at worst we've unnecessarily greyed the old
// pointer that was in src.
memmove(dst, src, typ.size)
if writeBarrier.cgo {
cgoCheckMemmove(typ, dst, src, 0, typ.size)
}
}
3、阻塞和恢復
3.1 傳送方被阻塞
假設 G2 需要很長時間的處理,在此期間,G1 不斷的傳送任務:
ch <- task1
ch <- task2
ch <- task3
但是當再一次 ch <- task4 的時候,由於 ch 的緩衝只有 3 個,所以沒有地方放了,於是 G1 被 block 了,當有人從佇列中取走一個 Task 的時候,G1 才會被恢復。這是我們都知道的,不過我們今天關心的不是發生了什麼,而是如何做到的?
3.2 goroutine 的執行時排程
首先,goroutine 不是作業系統執行緒,而是 使用者空間執行緒。因此 goroutine 是由 Go runtime 來建立並管理的,而不是 OS,所以要比作業系統執行緒輕量級。
當然,goroutine 最終還是要執行於某個執行緒中的,控制 goroutine 如何執行於執行緒中的是 Go runtime 中的 scheduler (排程器)。
Go 的執行時排程器是 M:N 排程模型,既 N 個 goroutine,會執行於 M 個 OS 執行緒中。換句話說,一個 OS 執行緒中,可能會執行多個 goroutine。
Go 的 M:N 排程中使用了3個結構:
- M: OS 執行緒
- G: goroutine
- P: 排程上下文
- P 擁有一個執行佇列,裡面是所有可以執行的 goroutine 及其上下文
要想執行一個 goroutine - G,那麼一個執行緒 M,就必須持有一個該 goroutine 的上下文 P。
3.3 goroutine 被阻塞的具體過程
那麼當 ch <- task4 執行的時候,channel 中已經滿了,需要 pause G1。這個時候:
- G1 會呼叫執行時的 gopark
- 然後 Go 的執行時排程器就會接管
- 將 G1 的狀態設定為 waiting
- 斷開 G1 和 M 之間的關係(switch out),因此 G1 脫離 M,換句話說,M 空閒了,可以安排別的任務了。
- 從 P 的執行佇列中,取得一個可執行的 goroutine G
- 建立新的 G 和 M 的關係(Switch in),因此 G 就準備好執行了。
- 當排程器返回的時候,新的 G 就開始執行了,而 G1 則不會執行,也就是 block 了。
從上面的流程中可以看到,對於 goroutine 來說,G1 被阻塞了,新的 G 開始執行了;而對於作業系統執行緒 M 來說,則根本沒有被阻塞。
我們知道 OS 執行緒要比 goroutine 要沉重的多,因此這裡儘量避免 OS 執行緒阻塞,可以提高效能。
3.4 goroutine 恢復執行的具體過程
前面理解了阻塞,那麼接下來理解一下如何恢復執行。不過,在繼續瞭解如何恢復之前,我們需要先進一步理解 hchan 這個結構。因為,當 channel 不在滿的時候,排程器是如何知道該讓哪個 goroutine 繼續執行呢?而且 goroutine 又是如何知道該從哪取資料呢?
在 hchan 中,除了之前提到的內容外,還定義有 sendq 和 recvq 兩個佇列,分別表示等待傳送、接收的 goroutine,及其相關資訊。
type hchan struct {
...
buf unsafe.Pointer // 指向一個環形佇列
...
sendq waitq // 等待傳送的佇列
recvq waitq // 等待接收的佇列
...
lock mutex // 互斥量
}
其中 waitq 是一個連結串列結構的佇列,每個元素是一個 sudog 的結構,其定義大致為:
type sudog struct {
g *g // 正在等候的 goroutine
elem unsafe.Pointer // 指向需要接收、傳送的元素
...
}
https://golang.org/src/runtime/runtime2.go?h=sudog#L270
所以在之前的阻塞 G1 的過程中,實際上:
- G1 會 給自己建立一個 sudog 的變數
- 然後追加到 sendq 的等候佇列中,方便將來的receiver 來使用這些資訊恢復 G1。
這些都是 發生在呼叫排程器之前。
那麼現在開始看一下如何恢復。
當 G2 呼叫 t := <- ch 的時候,channel 的狀態是,緩衝是滿的,而且還有一個 G1 在等候傳送佇列裡,然後 G2 執行下面的操作:
- G2 先執行 dequeue() 從緩衝佇列中取得 task1 給 t
- G2 從 sendq 中彈出一個等候傳送的 sudog
- 將彈出的 sudog 中的 elem 的值 enqueue() 到 buf 中
- 將彈出的 sudog 中的 goroutine,也就是 G1,狀態從 waiting 改為 runnable
- 然後,G2 需要通知排程器 G1 已經可以進行排程了,因此呼叫 goready(G1)。
- 排程器將 G1 的狀態改為 runnable
- 排程器將 G1 壓入 P 的執行佇列,因此在將來的某個時刻排程的時候,G1 就會開始恢復執行。
- 返回到 G2
注意,這裡是由 G2 來負責將 G1 的 elem 壓入 buf 的,這是一個優化。這樣將來 G1 恢復執行後,就不必再次獲取鎖、enqueue()、釋放鎖了。這樣就避免了多次鎖的開銷。
3.5 如果接收方先阻塞呢?
更酷的地方是接收方先阻塞的流程。
如果 G2 先執行了 t := <- ch,此時 buf 是空的,因此 G2 會被阻塞,他的流程是這樣:
- G2 給自己建立一個 sudog 結構變數。其中 g 是自己,也就是 G2,而 elem 則指向 t
- 將這個 sudog 變數壓入 recvq 等候接收佇列
- G2 需要告訴 goroutine,自己需要 pause 了,於是呼叫 gopark(G2)
- 和之前一樣,排程器將其 G2 的狀態改為 waiting
- 斷開 G2 和 M 的關係
- 從 P 的執行佇列中取出一個 goroutine
- 建立新的 goroutine 和 M 的關係
- 返回,開始繼續執行新的 goroutine
這些應該已經不陌生了,那麼當 G1 開始傳送資料的時候,流程是什麼樣子的呢?
G1 可以將 enqueue(task),然後呼叫 goready(G2)。不過,我們可以更聰明一些。
我們根據 hchan 結構的狀態,已經知道 task 進入 buf 後,G2 恢復執行後,會讀取其值,複製到 t 中。那麼 G1 可以根本不走 buf,G1 可以直接把資料給 G2。
Goroutine 通常都有自己的棧,互相之間不會訪問對方的棧內資料,除了 channel。這裡,由於我們已經知道了 t 的地址(通過 elem指標),而且由於 G2 不在執行,所以我們可以很安全的直接賦值。當 G2 恢復執行的時候,既不需要再次獲取鎖,也不需要對 buf 進行操作。從而節約了記憶體複製、以及鎖操作的開銷。
4、總結
- goroutine-safe
- hchan 中的 lock mutex
- 儲存、傳遞值,FIFO
- 通過 hchan 中的環形緩衝區來實現
- 導致 goroutine 的阻塞和恢復
- hchan 中的 sendq和recvq,也就是 sudog 結構的連結串列佇列
- 呼叫執行時排程器 (gopark(), goready())
四、其它 channel 的操作
1、無緩衝 channel
無緩衝的 channel 行為就和前面說的直接傳送的例子一樣:
- 接收方阻塞 → 傳送方直接寫入接收方的棧
- 傳送方阻塞 → 接受法直接從傳送方的 sudog 中讀取
2、select ##
https://golang.org/src/runtime/select.go
- 先把所有需要操作的 channel 上鎖
- 給自己建立一個 sudog,然後新增到所有 channel 的 sendq或recvq(取決於是傳送還是接收)
- 把所有的 channel 解鎖,然後 pause 當前呼叫 select 的 goroutine(gopark())
- 然後當有任意一個 channel 可用時,select 的這個 goroutine 就會被排程執行。
- resuming mirrors the pause sequence
五、為什麼 Go 會這樣設計?
1、Simplicity
更傾向於帶鎖的佇列,而不是無鎖的實現。
“效能提升不是憑空而來的,是隨著複雜度增加而增加的。” - dvyokov
後者雖然效能可能會更好,但是這個優勢,並不一定能夠戰勝隨之而來的實現程式碼的複雜度所帶來的劣勢。
2、Performance
- 呼叫 Go 執行時排程器,這樣可以保持 OS 執行緒不被阻塞跨 goroutine 的棧讀、寫。
- 可以讓 goroutine 醒來後不必獲取鎖
- 可以避免一些記憶體複製
當然,任何優勢都會有其代價。這裡的代價是實現的複雜度,所以這裡有更復雜的記憶體管理機制、垃圾回收以及棧收縮機制。
在這裡效能的提高優勢,要比複雜度的提高帶來的劣勢要大。
所以在 channel 實現的各種程式碼中,我們都可以見到這種 simplicity vs performance 的權衡後的結果。
原文地址:
視訊筆記:理解 channels - Kavya Joshi
推薦閱讀:
goroutine與排程器
個人微信公眾號:
作者:jiankunking 出處:http://blog.csdn.net/jiankunking
相關文章
- 理解 Go Channels[精品長文]Go
- tableau視訊筆記(一)筆記
- laracast 視訊教程系列筆記更新AST筆記
- 小程式視訊專案筆記筆記
- Channels 通道 - Go 學習記錄Go
- 王德峰視訊哲學課筆記筆記
- JVM狂神說視訊學習筆記JVM筆記
- ruby on rails筆記和理解AI筆記
- 《音視訊開發進階指南》讀書筆記(一) —— 音視訊基礎概念筆記
- batch normalization學習理解筆記BATORM筆記
- Flutter 例項 - 從本地到Flutter通訊 - Event ChannelsFlutter
- [視訊版]-Golang深入理解GMPGolang
- IoC(控制反轉)的理解筆記筆記
- 筆記bs,資訊筆記
- 學習韓順平細說Servlet/Jsp視訊筆記(一)ServletJS筆記
- MySQL筆記 13 檢視MySql筆記
- channels 版本問題
- Vue.js 2.0之全家桶系列視訊課程——筆記(五)Vue.js筆記
- Vue.js 2.0之全家桶系列視訊課程——筆記(四)Vue.js筆記
- SQLServer學習筆記 - 主鍵的理解SQLServer筆記
- Python學習筆記|Python之yield理解Python筆記
- “平板電視”學習筆記筆記
- 使用Django-Channels實現websocket通訊+大模型對話DjangoWeb大模型
- IMU與視覺資訊融合—手寫VIO課程筆記2(下)視覺筆記
- PyCon 2018: 中文視訊(1):理解位元組碼
- 【學習筆記】CSS深入理解之margin筆記CSS
- 【學習筆記】CSS深入理解之overflow筆記CSS
- 【學習筆記】CSS深入理解之relative筆記CSS
- 最容易理解的正規表示式筆記筆記
- GOT & PLT 易於理解的個人筆記Go筆記
- 騰訊雲使用筆記一: 騰訊雲重灌記錄筆記
- SPI通訊協議筆記協議筆記
- IIC通訊協議筆記協議筆記
- MySQL 8.0 視窗函式-筆記MySql函式筆記
- 《python運維和開發實戰-高階篇》視訊課程筆記Python運維筆記
- Python從入門到精通視訊(全60集)馬哥教育視訊(已修復部分視訊無聲音的問題+其他優化)+筆記分享Python優化筆記
- 騰訊視訊編譯優化記錄編譯優化
- (學習筆記)python 對__init__的初步理解筆記Python
- 《深入理解C#(第3版)》筆記1C#筆記