設(shè)計(jì)理念
執(zhí)行業(yè)務(wù)處理的 goroutine 不要通過(guò)共享內(nèi)存的方式通信慢逾,而是要通過(guò) Channel 通信的方式分享數(shù)據(jù)。
Channel 類(lèi)型和基本并發(fā)原語(yǔ)是有競(jìng)爭(zhēng)關(guān)系的,它應(yīng)用于并發(fā)場(chǎng)景朝卒,涉及到 goroutine 之間的通訊,可以提供并發(fā)的保護(hù)乐埠,等等抗斤。
應(yīng)用場(chǎng)景五種類(lèi)型
- 數(shù)據(jù)交流:當(dāng)作并發(fā)的 buffer 或者 queue,解決生產(chǎn)者 - 消費(fèi)者問(wèn)題丈咐。多個(gè) goroutine 可以并發(fā)當(dāng)作生產(chǎn)者(Producer)和消費(fèi)者(Consumer)瑞眼。
- 數(shù)據(jù)傳遞:一個(gè) goroutine 將數(shù)據(jù)交給另一個(gè) goroutine,相當(dāng)于把數(shù)據(jù)的擁有權(quán) (引用) 托付出去棵逊。
- 信號(hào)通知:一個(gè) goroutine 可以將信號(hào) (closing伤疙、closed、data ready 等) 傳遞給另一個(gè)或者另一組 goroutine 。
- 任務(wù)編排:可以讓一組 goroutine 按照一定的順序并發(fā)或者串行的執(zhí)行徒像,這就是編排的功能黍特。
- 鎖:利用 Channel 也可以實(shí)現(xiàn)互斥鎖的機(jī)制。
- 控制并發(fā)執(zhí)行的goroutine數(shù)量锯蛀,例如令牌桶灭衷。
基本用法
Channel 分為只能接收、只能發(fā)送旁涤、既可以接收又可以發(fā)送三種類(lèi)型翔曲。下面是它的語(yǔ)法定義:
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
相應(yīng)地,Channel 的正確語(yǔ)法如下:
chan string // 可以發(fā)送接收string
chan<- struct{} // 只能發(fā)送struct{}
<-chan int // 只能從chan接收int
通過(guò) make劈愚,我們可以初始化一個(gè) chan瞳遍,未初始化的 chan 的零值是 nil。你可以設(shè)置它的容量造虎,比如下面的 chan 的容量是 9527傅蹂,我們把這樣的 chan 叫做 buffered chan纷闺;如果沒(méi)有設(shè)置算凿,它的容量是 0,我們把這樣的 chan 叫做 unbuffered chan犁功。
make(chan int, 9527)
如果 chan 中還有數(shù)據(jù)氓轰,那么,從這個(gè) chan 接收數(shù)據(jù)的時(shí)候就不會(huì)阻塞浸卦,如果 chan 還未滿(mǎn)(“滿(mǎn)”指達(dá)到其容量)署鸡,給它發(fā)送數(shù)據(jù)也不會(huì)阻塞,否則就會(huì)阻塞限嫌。unbuffered chan 只有讀寫(xiě)都準(zhǔn)備好之后才不會(huì)阻塞靴庆,這也是很多使用 unbuffered chan 時(shí)的常見(jiàn) Bug。
nil 是 chan 的零值怒医,是一種特殊的 chan炉抒,對(duì)值是 nil 的 chan 的發(fā)送接收調(diào)用者總是會(huì)阻塞。
for + select + case + chan
func main() {
var ch = make(chan int, 10)
for i := 0; i < 10; i++ {
select {
case ch <- i:
case v := <-ch:
fmt.Println(v)
}
}
}
使用for + select循環(huán)監(jiān)控多個(gè)chan的情況稚叹,一旦發(fā)送成功 or 接收數(shù)據(jù)成功則會(huì)完成一次循環(huán)
for chan
for v := range ch {
fmt.Println(v)
}
可以一直從ch中等待讀取數(shù)據(jù)焰薄,直到ch關(guān)閉才會(huì)退出循環(huán)
實(shí)現(xiàn)原理
- qcount:代表 chan 中已經(jīng)接收但還沒(méi)被取走的元素的個(gè)數(shù)。內(nèi)建函數(shù) len 可以返回這個(gè)字段的值扒袖。- dataqsiz:隊(duì)列的大小塞茅。chan 使用一個(gè)循環(huán)隊(duì)列來(lái)存放元素,循環(huán)隊(duì)列很適合這種生產(chǎn)者 - 消費(fèi)者的場(chǎng)景(我很好奇為什么這個(gè)字段省略 size 中的 e)季率。
- buf:存放元素的循環(huán)隊(duì)列的 buffer野瘦。
- elemtype 和 elemsize:chan 中元素的類(lèi)型和 size。因?yàn)?chan 一旦聲明飒泻,它的元素類(lèi)型是固定的鞭光,即普通類(lèi)型或者指針類(lèi)型啊掏,所以元素大小也是固定的。
- sendx:處理發(fā)送數(shù)據(jù)的指針在 buf 中的位置衰猛。一旦接收了新的數(shù)據(jù)迟蜜,指針就會(huì)加上 elemsize,移向下一個(gè)位置啡省。buf 的總大小是 elemsize 的整數(shù)倍娜睛,而且 buf 是一個(gè)循環(huán)列表。
- recvx:處理接收請(qǐng)求時(shí)的指針在 buf 中的位置卦睹。一旦取出數(shù)據(jù)畦戒,此指針會(huì)移動(dòng)到下一個(gè)位置。
- recvq:chan 是多生產(chǎn)者多消費(fèi)者的模式结序,如果消費(fèi)者因?yàn)闆](méi)有數(shù)據(jù)可讀而被阻塞了障斋,就會(huì)被加入到 recvq 隊(duì)列中。
- sendq:如果生產(chǎn)者因?yàn)?buf 滿(mǎn)了而阻塞徐鹤,會(huì)被加入到 sendq 隊(duì)列中垃环。
send、recv返敬、close流程
本質(zhì)是 "值的拷貝"
發(fā)送操作步驟
- 在發(fā)送數(shù)據(jù)的邏輯執(zhí)行之前會(huì)先為當(dāng)前 Channel 加鎖遂庄,防止多個(gè)線(xiàn)程并發(fā)修改數(shù)據(jù)
- 如果當(dāng)前 Channel 的 recvq 上存在已經(jīng)被阻塞的 Goroutine,那么會(huì)直接將數(shù)據(jù)發(fā)送給當(dāng)前 Goroutine 并將其設(shè)置成下一個(gè)運(yùn)行的 Goroutine
- 如果recvq為空,則判斷緩沖區(qū)是否可寫(xiě),可寫(xiě)則從當(dāng)前goroutine復(fù)制數(shù)據(jù)到緩沖區(qū)中
- 如果不滿(mǎn)足上面的兩種情況劲赠,會(huì)創(chuàng)建一個(gè) runtime.sudog 結(jié)構(gòu)并將其加入 Channel 的 sendq 隊(duì)列中涛目,當(dāng)前 Goroutine 也會(huì)陷入阻塞等待其他的協(xié)程從 Channel 接收數(shù)據(jù)
- 寫(xiě)入完成釋放鎖.
發(fā)送數(shù)據(jù)的過(guò)程中包含幾個(gè)會(huì)觸發(fā) Goroutine 調(diào)度的時(shí)機(jī):
- 發(fā)送數(shù)據(jù)時(shí)發(fā)現(xiàn) Channel 上存在等待接收數(shù)據(jù)的 Goroutine,立刻設(shè)置處理器的 runnext 屬性凛澎,但是并不會(huì)立刻觸發(fā)調(diào)度霹肝;
- 發(fā)送數(shù)據(jù)時(shí)并沒(méi)有找到接收方并且緩沖區(qū)已經(jīng)滿(mǎn)了,這時(shí)會(huì)將自己加入 Channel 的 sendq 隊(duì)列并調(diào)用 runtime.goparkunlock 觸發(fā) Goroutine 的調(diào)度讓出處理器的使用權(quán)塑煎;
讀取/接收操作步驟
- 在接收數(shù)據(jù)的邏輯執(zhí)行之前會(huì)先為當(dāng)前 Channel 加鎖沫换,防止多個(gè)線(xiàn)程并發(fā)修改數(shù)據(jù);
- 如果 Channel 為空,那么會(huì)直接調(diào)用 runtime.gopark 掛起當(dāng)前 Goroutine;
- 如果 Channel 已經(jīng)關(guān)閉并且緩沖區(qū)沒(méi)有任何數(shù)據(jù)轧叽,runtime.chanrecv 會(huì)直接返回;
- 如果 Channel 的 sendq 隊(duì)列中存在掛起的 Goroutine, 如果不存在緩沖區(qū),則將 Channel 發(fā)送隊(duì)列中 Goroutine 存儲(chǔ)的 elem 數(shù)據(jù)拷貝到目標(biāo)內(nèi)存地址中; 如果存在緩沖區(qū)苗沧; 將隊(duì)列頭中的數(shù)據(jù)拷貝到接收方的內(nèi)存地址; 然后將發(fā)送隊(duì)列下一個(gè)的數(shù)據(jù)拷貝到緩沖區(qū)中炭晒,釋放一個(gè)阻塞的發(fā)送方待逞;
- 如果 Channel 的緩沖區(qū)中包含數(shù)據(jù),那么直接讀取 recvx 索引對(duì)應(yīng)的數(shù)據(jù);
- 在默認(rèn)情況下會(huì)掛起當(dāng)前的 Goroutine网严,將 runtime.sudog 結(jié)構(gòu)加入 recvq 隊(duì)列并陷入休眠等待調(diào)度器的喚醒;
- 讀取完成后釋放鎖.
總結(jié)一下從 Channel 接收數(shù)據(jù)時(shí)识樱,會(huì)觸發(fā) Goroutine 調(diào)度的兩個(gè)時(shí)機(jī):
- 當(dāng) Channel 為空時(shí);
- 當(dāng)緩沖區(qū)中不存在數(shù)據(jù)并且也不存在數(shù)據(jù)的發(fā)送者時(shí).
關(guān)閉流程
- 加鎖.
- 接著把所有掛在這個(gè) channel 上的 sender 和 receiver 全都連成一個(gè) sudog 鏈表
- 解鎖.
- 最后,再將所有的 sudog 全都喚醒.
- 對(duì)于等待接收者而言怜庸,會(huì)收到一個(gè)相應(yīng)類(lèi)型的零值当犯。對(duì)于等待發(fā)送者,會(huì)直接 panic割疾。所以嚎卫,在不了解 channel 還有沒(méi)有發(fā)送者的情況下,不能貿(mào)然關(guān)閉 channel. 所以最好是由唯一的channel發(fā)送者去執(zhí)行關(guān)閉操作.
關(guān)閉后
關(guān)閉流程僅僅是做一些標(biāo)記和通知操作, 實(shí)際上沒(méi)有回收掉這個(gè)chan, chan依然是可讀的, 當(dāng)讀到第二個(gè)字段為false/nil時(shí), 代表chanel已經(jīng)關(guān)閉, 通道沒(méi)有數(shù)據(jù). 對(duì)于一個(gè) channel宏榕,如果最終沒(méi)有任何 goroutine 引用它拓诸,不管 channel 有沒(méi)有被關(guān)閉,最終都會(huì)被 gc 回收
一個(gè)泄漏資源例子
func process(timeout time.Duration) bool {
ch := make(chan bool)
go func() {
// 模擬處理耗時(shí)的業(yè)務(wù)
time.Sleep((timeout + time.Second))
ch <- true // block
fmt.Println("exit goroutine")
}()
select {
case result := <-ch:
return result
case <-time.After(timeout):
return false
}
}
在這個(gè)例子中麻昼,process 函數(shù)會(huì)啟動(dòng)一個(gè) goroutine奠支,去處理需要長(zhǎng)時(shí)間處理的業(yè)務(wù),處理完之后抚芦,會(huì)發(fā)送 true 到 chan 中倍谜,目的是通知其它等待的 goroutine,可以繼續(xù)處理了叉抡。
主 goroutine 接收到任務(wù)處理完成的通知尔崔,或者超時(shí)后就返回了。
如果發(fā)生超時(shí)卜壕,process 函數(shù)就返回了您旁,這就會(huì)導(dǎo)致 unbuffered 的 chan 從來(lái)就沒(méi)有被讀取烙常。我們知道轴捎,unbuffered chan 必須等 reader 和 writer 都準(zhǔn)備好了才能交流,否則就會(huì)阻塞蚕脏。超時(shí)導(dǎo)致未讀侦副,結(jié)果就是子 goroutine 就阻塞在第 7 行永遠(yuǎn)結(jié)束不了,進(jìn)而導(dǎo)致 goroutine 泄漏驼鞭。
解決這個(gè) Bug 的辦法很簡(jiǎn)單秦驯,就是將 unbuffered chan 改成容量為 1 的 chan,這樣第 7 行就不會(huì)被阻塞了挣棕。
chan 與 傳統(tǒng)并發(fā)原語(yǔ)選擇
- 共享資源的并發(fā)訪(fǎng)問(wèn)使用傳統(tǒng)并發(fā)原語(yǔ)译隘;
- 復(fù)雜的任務(wù)編排和消息傳遞使用 Channel;
- 消息通知機(jī)制使用 Channel洛心,除非只想 signal 一個(gè) goroutine固耘,才使用 Cond;
- 簡(jiǎn)單等待所有任務(wù)的完成用 WaitGroup词身,也有 Channel 的推崇者用 Channel厅目,都可以;
- 需要和 Select 語(yǔ)句結(jié)合,使用 Channel损敷;
- 需要和超時(shí)配合時(shí)葫笼,使用 Channel 和 Context。
panic情況匯總
文章來(lái)源
<<極客時(shí)間>>Go 并發(fā)編程實(shí)戰(zhàn)課13講