上一篇:Golang channel 之 數(shù)據(jù)結(jié)構(gòu)
下一篇:Golang channel 之 讀操作 recv
channel的常規(guī)寫操作
假如有一個(gè)元素類型為int的channel,變量名為ch理肺,那么常規(guī)的寫操作(簡稱send為寫)在代碼中的寫法如下所示:
ch <- 10
其中ch可能是“有緩沖”的,也可能是“無緩沖”的善镰,甚至可能為nil妹萨。
按照上面的寫法,有兩種情況能使寫操作不會(huì)阻塞:
1)通道ch的recvq里已有g(shù)oroutine在等待炫欺;
2)通道ch是“有緩沖”的乎完,且緩沖區(qū)沒有用盡。
第一種情況中品洛,只要ch的recvq里有協(xié)程在排隊(duì)树姨,當(dāng)前協(xié)程就直接把數(shù)據(jù)交給recvq隊(duì)首的那個(gè)協(xié)程就好了,然后兩個(gè)協(xié)程都可以繼續(xù)執(zhí)行桥状,無關(guān)ch有沒有緩沖娃弓;
第二種情況中,ch是有緩沖的岛宦,且緩沖區(qū)沒有用盡,也就是底層數(shù)組沒有存滿耍缴,那么當(dāng)前協(xié)程直接把數(shù)據(jù)追加到緩沖數(shù)組中砾肺,就可以繼續(xù)執(zhí)行。
同樣是上面的寫法防嗡,有三種情況會(huì)使寫操作阻塞:
1)通道ch為nil变汪;
2)通道ch無緩沖且recvq為空;
3)通道ch有緩沖且緩沖區(qū)已用盡蚁趁。
第一種情況中裙盾,參照golang的實(shí)現(xiàn),允許對(duì)nil通道執(zhí)行寫操作他嫡,但是會(huì)使當(dāng)前協(xié)程永久性的阻塞在這個(gè)nil通道上番官,例如如下代碼會(huì)因死鎖拋出異常:
package main
func main() {
var ch chan int
ch <- 10
}
第二種情況中,ch為無緩沖通道钢属,recvq中沒有協(xié)程在等待徘熔,所以當(dāng)前協(xié)程需要到通道的sendq中排隊(duì);
第三種情況中淆党,ch有緩沖且已用盡酷师,隱含的信息就是recvq為空讶凉,否則緩沖不會(huì)用盡,所以當(dāng)前協(xié)程只能到sendq中排隊(duì)山孔。
channel的非阻塞寫操作
熟悉并發(fā)編程的同學(xué)應(yīng)該知道懂讯,有些鎖是支持tryLock操作的,也就是我想獲得這把鎖台颠,但是萬一已經(jīng)被別人獲得了褐望,我不阻塞等待,可以去干其他事情蓉媳。
對(duì)于通道的非阻塞寫就是:我想向通道寫數(shù)據(jù)譬挚,但是如果當(dāng)前沒有讀者在排隊(duì)等待,且緩沖區(qū)沒有剩余空間(包含無緩沖)酪呻,我就需要阻塞等待减宣。但是我不想等待,所以立刻返回并告訴我“現(xiàn)在不能寫”就可以了玩荠。
在golang中漆腌,對(duì)于單個(gè)通道的非阻塞寫操作可以用如下代碼實(shí)現(xiàn),注意是一個(gè)select阶冈、一個(gè)case和一個(gè)default闷尿,都是一個(gè),不能多也不能少:
select {
case ch <- 10:
...
default:
...
}
如果檢測到寫ch不會(huì)阻塞女坑,那么就會(huì)執(zhí)行case ch <- 10:
分支填具,如果會(huì)阻塞,就會(huì)執(zhí)行default:
分支匆骗。關(guān)于什么情況下會(huì)阻塞劳景,什么情況下不會(huì)阻塞,參見上面的情況分析碉就。
channel寫操作的實(shí)現(xiàn)
上面簡單的分析了channel的常規(guī)寫操作和非阻塞寫操作盟广,雖然兩者在形式上看起來稍微有些差異,但是主要邏輯都是通過runtime.chansend函數(shù)實(shí)現(xiàn)的瓮钥,下面簡單的進(jìn)行一下解讀:
首先來看一下chansend函數(shù)的原型:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool
其中:
c是一個(gè)hchan指針筋量,指向要用來send數(shù)據(jù)的channel;
ep是一個(gè)指針碉熄,指向要被寫入通道c的數(shù)據(jù)桨武,數(shù)據(jù)類型要和c的元素類型一致;
block表示如果寫操作不能立即完成锈津,是否想要阻塞等待玻募;
callerpc用以進(jìn)行race相關(guān)檢測,暫時(shí)不需要關(guān)心一姿;
返回值為true表示數(shù)據(jù)寫入完成七咧,false表示目前不可寫但因?yàn)椴幌胱枞╞lock為false)而返回跃惫。
chansend函數(shù)的主要邏輯如下:
如果c等于nil {
如果不想阻塞 {
return false
}
永久阻塞
}
如果(不想阻塞 且 c未關(guān)閉) 且 ((c無緩沖 且 recvq是空的) 或 (c有緩沖 且 緩沖區(qū)已滿)) {
return false
}
對(duì)c加鎖
如果c已關(guān)閉 {
解鎖c
panic("send on closed channel")
}
如果recvq中有內(nèi)容 {
取出隊(duì)首協(xié)程,并把數(shù)據(jù)傳遞給它并解鎖c
return true
}
如果緩沖區(qū)還有空間 {
把數(shù)據(jù)追加到緩沖區(qū)艾栋,移動(dòng)sendx爆存,遞增qcount
解鎖c
return true
}
如果不想阻塞 {
解鎖c
return false
}
進(jìn)入sendq排隊(duì)等待同時(shí)解鎖c,條件滿足時(shí)會(huì)完成數(shù)據(jù)傳遞并被喚醒
return true
逐塊對(duì)應(yīng)以上代碼:
1)如果c為nil蝗砾,進(jìn)一步判斷block:如果block為false先较,那么直接返回false,表示未send數(shù)據(jù)悼粮;如果block為true闲勺,那么就讓當(dāng)前協(xié)程”永久“的阻塞在這個(gè)nil通道上;
2)如果block為false且closed為0扣猫,也就是在”不想阻塞“且通道”未關(guān)閉“的前提下菜循,如果是通道”無緩沖“且recvq為空,或者是通道”有緩沖“且緩沖已用盡申尤,則直接返回false癌幕。本步判斷是在不加鎖的情況下進(jìn)行的,目的是讓非阻塞寫在無法立即完成時(shí)能真正不阻塞(加鎖可能阻塞)昧穿。此處有疑問的話勺远,可返回上面看常規(guī)寫操作的情況分析;
3)加鎖;
4)如果closed不為0时鸵,即通道已經(jīng)關(guān)閉的話胶逢,則解鎖,然后panic饰潜。因?yàn)椴豢梢詫懭胍殃P(guān)閉的通道初坠;
5)如果recvq不為空,就從中取出第一個(gè)排隊(duì)的協(xié)程囊拜,將數(shù)據(jù)傳遞給這個(gè)協(xié)程,并將該協(xié)程置為ready狀態(tài)(放入run queue比搭,進(jìn)而得到調(diào)度)冠跷,然后解鎖,返回true身诺;
6)通過比較qcount和dataqsiz判斷緩沖區(qū)是否還有剩余空間蜜托,在這里”無緩沖“的通道被視為沒有剩余空間。有剩余空間的話霉赡,將數(shù)據(jù)追加到緩沖區(qū)中橄务,移動(dòng)sendx,增加qcount穴亏,解鎖蜂挪,返回true重挑;
7)如果block為false,即不想阻塞棠涮,則解鎖谬哀,返回false;
8)最后严肪,到達(dá)這里就是阻塞寫了史煎,當(dāng)前協(xié)程把自己追加到通道的sendq中阻塞排隊(duì),同時(shí)解鎖驳糯,等到條件滿足時(shí)會(huì)被喚醒篇梭。
流程比較長,還是畫個(gè)圖吧:
本篇總結(jié)
1)channel的常規(guī)寫操作如c <- x
酝枢,會(huì)被編譯器轉(zhuǎn)換為對(duì)runtime.chansend1的調(diào)用恬偷,后者內(nèi)部只是調(diào)用了runtime.chansend;
2)非阻塞式的寫操作隧枫,即select喉磁、case、default三個(gè)一官脓,會(huì)被編譯器轉(zhuǎn)換為對(duì)runtime.selectnbsend的調(diào)用协怒,后者也僅僅是調(diào)用了runtime.chansend。非阻塞寫的實(shí)現(xiàn)效果如下:
select {
case c <- v:
... foo
default:
... bar
}
// 被編譯器轉(zhuǎn)化為:
if selectnbsend(c, v) {
... foo
} else {
... bar
}
上一篇:Golang channel 之 數(shù)據(jù)結(jié)構(gòu)
下一篇:Golang channel 之 讀操作 recv