channel是什么杨何?
golang語言中,channel是一個協(xié)程安全的FIFO的隊列,讀取和寫入操作都是原子操作
使用場景
用來做多協(xié)程之間的通信凛澎,java中的線程之間的通信是通過共享內(nèi)存實現(xiàn)的牍戚,A線程獲取內(nèi)存區(qū)域仿耽,并且“鎖”住內(nèi)存這塊區(qū)域,然后執(zhí)行臨界區(qū)代碼错负,這一時刻無法獲取鎖的其他線程阻塞,直到A線程釋放鎖(實際上就是釋放剛才鎖住的內(nèi)存區(qū)域)勇边,其他線程繼續(xù)競爭共享內(nèi)存犹撒,獲取鎖的執(zhí)行臨界區(qū)代碼。這個過程本質(zhì)上是通過共享內(nèi)存的方式實現(xiàn)多線程通信粥诫。而golang提出了新的通信方式:用通信來共享內(nèi)存姐呐,而不要用共享內(nèi)存來通信
使用方式
##無緩沖區(qū)的channel
創(chuàng)建 var NoRoutChannel chan 【類型】= make(chan 【類型】)
使用場景
只是作為信號的channel,告訴另一個協(xié)程谍珊,這件事我做完了搀别,而不需要給另外的協(xié)程序發(fā)送做完后的通知內(nèi)容
##有緩沖區(qū)的channel
創(chuàng)建 var NoRoutChannel chan 【類型】= make(chan 【類型】,【緩沖大小】)
使用場景
告訴另一個協(xié)程,這件事我做完了执赡,并且發(fā)送和另一個協(xié)程序傳遞的變量
PS:注意channel讀取和寫入操作必須在兩個不同的協(xié)程中進行镰踏,否則panic
channel狀態(tài)
channel分為nil、open沙合、closed
對于nil的channel無論讀寫都panic奠伪,對于closed狀態(tài)的channel,向里邊push的操作會報panic
demo: 使用channel實現(xiàn)一個生產(chǎn)者消費者模式
var wg sync.WaitGroup = sync.WaitGroup{}
type Product struct {
Queue chan string
}
func (product *Product) product(apple string){
fmt.Println("product: "+apple)
product.Queue <- apple
}
type Consumer struct {
Queue chan string
}
func (consumer *Consumer) Consume(){
con := <- consumer.Queue
fmt.Println("consume:"+con)
defer wg.Done()
}
func main() {
testChan := make(chan string,1)
p := Product{
Queue: testChan,
}
c := Consumer{
Queue: testChan,
}
go c.Consume()
go p.product("3333")
wg.Add(1)
wg.Wait()
fmt.Println("end job")
}
channel內(nèi)部原理
數(shù)據(jù)結(jié)構(gòu)
type hchan struct {
qcount uint // 所有在隊列中數(shù)據(jù)數(shù)量
dataqsiz uint // 環(huán)形隊列大小首懈,可以存放的元素個數(shù)
buf unsafe.Pointer // 只想環(huán)形隊列的指針
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // 生產(chǎn)下標(biāo)
recvx uint // 消費下標(biāo)
recvq waitq // 消費者隊列
sendq waitq // 生產(chǎn)者隊列
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
channel緩沖區(qū)實現(xiàn)--環(huán)形隊列
chan內(nèi)部實現(xiàn)了一個環(huán)形隊列作為其緩沖區(qū)绊率,隊列的長度是創(chuàng)建chan時指定的。
下圖展示了一個可緩存6個元素的channel示意圖:
- dataqsiz指示了隊列長度為6究履,即可緩存6個元素滤否;
- buf指向隊列的內(nèi)存,隊列中還剩余兩個元素最仑;
- qcount表示隊列中還有兩個元素藐俺;
- sendx指示后續(xù)寫入的數(shù)據(jù)存儲的位置炊甲,取值[0, 6);
- recvx指示從該位置讀取數(shù)據(jù), 取值[0, 6)欲芹;
等待隊列
從channel讀數(shù)據(jù)卿啡,如果channel緩沖區(qū)為空或者沒有緩沖區(qū),當(dāng)前goroutine會被阻塞菱父。
向channel寫數(shù)據(jù)颈娜,如果channel緩沖區(qū)已滿或者沒有緩沖區(qū),當(dāng)前goroutine會被阻塞滞伟。
被阻塞的goroutine將會掛在channel的等待隊列中:
- 因讀阻塞的goroutine會被向channel寫入數(shù)據(jù)的goroutine喚醒揭鳞;
- 因?qū)懽枞膅oroutine會被從channel讀數(shù)據(jù)的goroutine喚醒;
下圖展示了一個沒有緩沖區(qū)的channel梆奈,有幾個goroutine阻塞等待讀數(shù)據(jù):
注意野崇,一般情況下recvq和sendq至少有一個為空。只有一個例外亩钟,那就是同一個goroutine使用select語句向channel一邊寫數(shù)據(jù)乓梨,一邊讀數(shù)據(jù)。
make(chan string,2)分析
代碼:
func makechan(t *chantype, size int) *hchan {
var c hchan
c = new(hchan)
c.buf = malloc(元素類型大小size)
c.elemsize = 元素類型大小
c.elemtype = 元素類型
c.dataqsiz = size
return c
}
向channel寫數(shù)據(jù)
向一個channel中寫數(shù)據(jù)簡單過程如下:
- 如果等待接收隊列recvq不為空清酥,說明緩沖區(qū)中沒有數(shù)據(jù)或者沒有緩沖區(qū)扶镀,此時直接從recvq取出G,并把數(shù)據(jù)寫入,最后把該G喚醒焰轻,結(jié)束發(fā)送過程臭觉;
- 如果緩沖區(qū)中有空余位置,將數(shù)據(jù)寫入緩沖區(qū)辱志,結(jié)束發(fā)送過程蝠筑;
- 如果緩沖區(qū)中沒有空余位置,將待發(fā)送數(shù)據(jù)寫入G揩懒,將當(dāng)前G加入sendq什乙,進入睡眠,等待被讀goroutine喚醒已球;
簡單流程圖如下:
從channel中讀取數(shù)據(jù)
從一個channel讀數(shù)據(jù)簡單過程如下:
- 如果等待發(fā)送隊列sendq不為空臣镣,且沒有緩沖區(qū),直接從sendq中取出G智亮,把G中數(shù)據(jù)讀出忆某,最后把G喚醒,結(jié)束讀取過程阔蛉;
- 如果等待發(fā)送隊列sendq不為空弃舒,此時說明緩沖區(qū)已滿,從緩沖區(qū)中首部讀出數(shù)據(jù)馍忽,把G中數(shù)據(jù)寫入緩沖區(qū)尾部棒坏,把G喚醒,結(jié)束讀取過程遭笋;
- 如果緩沖區(qū)中有數(shù)據(jù)坝冕,則從緩沖區(qū)取出數(shù)據(jù),結(jié)束讀取過程瓦呼;
- 將當(dāng)前goroutine加入recvq喂窟,進入睡眠,等待被寫goroutine喚醒央串;
簡單流程圖如下:
關(guān)閉channel
關(guān)閉channel時會把recvq中的G全部喚醒磨澡,本該寫入G的數(shù)據(jù)位置為nil。把sendq中的G全部喚醒质和,但這些G會panic稳摄。
除此之外,panic出現(xiàn)的常見場景還有:
關(guān)閉值為nil的channel
關(guān)閉已經(jīng)被關(guān)閉的channel
向已經(jīng)關(guān)閉的channel寫數(shù)據(jù)