go 通道-channel、協程-routine、sync
golang 裏不需要學習如何創建維護進程池/線程池,也不需要分析什麼情況使用多線程,什麼情況使用多進程,因為你沒得選。
當然,也不需要選。
go原生的 goroutine(協程)已足夠優秀,能自動幫你處理好所有事情,而你要做的只是執行它,so easy...
goroutine 也是go天生支持高併發的底氣。
goroutine 奉行通過通信來共享內存,而不是共享內存來通信。
參考:https://www.topgoer.com/併發編程/併發介紹.html
goroutine 可以簡單類比為一個函數:
直接調用時,它就是一個普通函數;
如果在調用前加一個關鍵字go,那你就開啓了一個 goroutine,開啓了一扇新世界的大門。
下面看一個示例:
routineTest := func(rName string) {
for i := 0; i < 5; i++ {
fmt.Println("Goroutine: ", rName, "; idx: ", i)
time.Sleep(20 * time.Millisecond)
}
}
go routineTest("協程1號")
go routineTest("協程2號")
fmt.Println("hello,world... 編寫順序在 Go-Routine 之後")
如果直接執行你會發現,只輸出了 hello,world 這一行,並沒有按預期的輸出 協程1號和2號;
這是因為協程的創建需要時間,hello,world 打印後,協程還沒來得及執行,就結束了。
為了便於觀測,我們追加一行 sleep,以阻塞 main 的執行,保證協程的輸出;
go routineTest("協程1號")
go routineTest("協程2號")
fmt.Println("hello,world... 編寫順序在 Go-Routine 之後")
time.Sleep(time.Second) // 追加一行 sleep 以阻塞 main 的執行
這時成功輸出了 協程1號和2號 的信息,但你會發現它的輸出是無序的(無序才是符合預期的,如同兩個線程一樣,併發執行);
當然了,真正的併發程序不能依賴sleep,需要結合 channel(信道) 來實現。
通道-channel
channel是一種類型,一種引用類型。聲明通道類型的格式:var 變量 chan 元素類型;
通道有 發送(send)、接收(receive)和關閉(close) 三種操作;
發送和接收都使用 <- 符號,箭頭指向誰,就代表由誰來接收。
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i < 6; i++ {
// 向channel發送消息
ch1 <- i
}
// 如果你的管道不往裏存值或者取值的時候一定記得關閉管道
close(ch1)
}()
go func() {
for {
d, ok := <-ch1 // 判斷通道是否被關閉:通道關閉後再取值ok=false
if !ok {
break
}
fmt.Println("從ch1接收[", d, "]賦值到ch2")
ch2 <- d
}
close(ch2)
}()
for v := range ch2 {
// 判斷通道是否被關閉:通道關閉後會退出 for range 循環
fmt.Println("ch2接收到:", v)
}
fmt.Println("main-完成")
Tips:
通道關閉需要注意的是:只有在通知接收方goroutine所有的數據都發送完畢的時候才需要關閉通道。
`通道是可以被垃圾回收機制回收的`,它和關閉文件是不一樣的,在結束操作之後關閉文件是必須要做的,但關閉通道不是必須的。
另外需要注意的是,上面的通道創建方式,make(chan int) 創建的是 無緩衝的通道,必須顯式定義接收方goroutine之後才能發送值;
就好比你的小區沒有快遞櫃,那麼快遞員只能和你電話確認有人在家接收的時候,才能給你派送。所以如下的方式執行會報deadlock錯誤:
ch := make(chan int)
fmt.Println("無緩衝channel-死鎖測試")
ch <- 1
<-ch
報錯內容:fatal error: all goroutines are asleep - deadlock!
這裏發送和接收,無論誰先執行,都會阻塞以等待另一個 goroutine 來執行;
無緩衝 channel,發送者會阻塞,直到接收者接收了發送的值; 所以這裏串行執行,會導致死鎖;
因為,無緩衝 channel,要求在消息發送時需要接收者已就緒,很顯然單協程無法滿足,因為會導致發送和接收串行。
如果想讓如上實例運行成功,需要顯示指定通道的size-容量: make(chan int, 3),這裏的3就是指容量3;
只要給定的通道容量大於0,就代表定義的是 有緩衝的通道,這時上面的實例就可以成功執行了。
為啥可以呢?
這就好比,快遞公司在你的小區建立了一個快遞櫃,櫃子的容量是3,只要有空餘的櫃子,快遞員就可以把快遞扔到櫃子裏,他就可以收工了。
但是,有緩衝的chan也不是萬能的,如果chan的緩衝區滿了,這時再寫入依然會阻塞,比如下面的情況,也會dead-lock:
ch := make(chan int, 1) // 設置緩衝區大小為 1
fmt.Println("有緩衝channel-死鎖測試")
ch <- 1
// 第二次寫入時緩衝區已滿,阻塞,然後死鎖;
ch <- 1
這就相當於小區的快遞櫃只能容納一件快遞,如果快遞員帶着兩件過來,就只能死等了。
引申:單向通道
説到快遞櫃,這裏就有一個問題,快遞櫃只允許快遞員投放快遞,而不允許用户自己投;
也就是説,它的使用場景是單向的,即 單向通道:
單向發送:`chan<- int` 代表一個只能向chan發送消息的通道,不能接收來自chan的消息;
單向接收:`<-chan int` 代表一個只能接收來自chan的消息的通道,但不能向chan發消息。
其實很好理解:
箭頭指向 chan,代表僅能將消息發給 chan,相當於 chan 只寫;
箭頭從chan向外指,代表僅能單向接收來自 chan 的消息,相當於 chan 只讀;
參考示例:
// 單向發送的channel:僅能向chan發送消息,相當於 chan 只寫
sendOnlyChan := func(in chan<- int) {
for i := 1; i < 5; i++ {
in <- i
}
close(in)
}
// 單向接收的channel:僅能接收來自chan的消息,相當於 chan 只讀
recvOnlyChan := func(out <-chan int) {
for v := range out {
fmt.Println("當前接收到來自chan的消息:", v)
}
}
ch := make(chan int)
go sendOnlyChan(ch)
recvOnlyChan(ch)
參考文檔:https://www.topgoer.com/併發編程/channel.html
=====