golang?channel多協(xié)程通信常用方法底層原理全面解析
一、channel 的概念
channel 是一個通道,用于端到端的數(shù)據(jù)傳輸,這有點像我們平常使用的消息隊列,只不過 channel 的發(fā)送方和接受方是 goroutine 對象,屬于內存級別的通信。
這里涉及到了 goroutine 概念,goroutine 是輕量級的協(xié)程,有屬于自己的??臻g。 我們可以把它理解為線程,只不過 goroutine 的性能開銷很小,并且在用戶態(tài)上實現(xiàn)了屬于自己的調度模型。
傳統(tǒng)的線程通信有很多方式,像內存共享、信號量等。其中內存共享實現(xiàn)較為簡單,只需要對變量進行并發(fā)控制,加鎖即可。但這種在后續(xù)業(yè)務逐漸復雜時,將很難維護,耦合性也比較強。
后來提出了 CSP 模型,即在通信雙方抽象出中間層,數(shù)據(jù)的流轉由中間層來控制,通信雙方只負責數(shù)據(jù)的發(fā)送和接收,從而實現(xiàn)了數(shù)據(jù)的共享,這就是所謂的通過通信來共享內存。 channel 就是按這個模型來實現(xiàn)的。
channel 在多并發(fā)操作里是屬于協(xié)程安全的,并且遵循了 FIFO 特性。即先執(zhí)行讀取的 goroutine 會先獲取到數(shù)據(jù),先發(fā)送數(shù)據(jù)的 goroutine 會先輸入數(shù)據(jù)。
另外,channel 的使用將會引起 Go runtime 的調度調用,會有阻塞和喚起 goroutine 的情況產生。
二、channel 的使用
在深入了解 channel 的底層之前,我們先來看看 channel 的常用用法。
channel 的創(chuàng)建
ch := make(chan int)
上面是創(chuàng)建了無緩沖的 channel,一旦有 goroutine 往 channel 發(fā)送數(shù)據(jù),那么當前的 goroutine 會被阻塞住,直到有其他的 goroutine 消費了 channel 里的數(shù)據(jù),才能繼續(xù)運行。
還有另外一種是有緩沖的 channel,它的創(chuàng)建是這樣的:
ch := make(chan int, 2)
第二個參數(shù)表示 channel 可緩沖數(shù)據(jù)的容量。只要當前 channel 里的元素總數(shù)不大于這個可緩沖容量,則當前的 goroutine 就不會被阻塞住。
需要注意的是,上面 make 后返回的是一個指向 hchan 結構的指針變量,等會將會聊聊 hchan 的底層結構。
另外,我們也可以聲明一個 nil 的 channel,只是創(chuàng)建這樣的 channel 沒有意義,讀、寫 channel 都將會被阻塞住。一般 nil channel 用在 select 上,讓 select 不再從這個 channel 里讀取數(shù)據(jù),如下用法:
ch1 := make(chan int) ch2 := make(chan int) go func() { if !ok { // 某些原因,設置 ch1 為 nil ch1 = nil } }() for { select { case <-ch1: // 當 ch1 被設置為 nil 后,將不會到達此分支了。 doSomething1() case <-ch2: doSomething2() } }
使用 channel 時我們還可以控制 channel 只讀只寫操作:
func readChan(ch <-chan int){ // chan 只允許被讀 } func main(){ ch := make(chan int) readChan(ch) }
反之,如果要求只寫操作,則可以這樣:
func writeChan(ch chan<- int){ // chan 只允許被寫 }
channel 的讀寫
往一個 channel 發(fā)送數(shù)據(jù),可以這樣寫:
ch := make(chan int) ch <- 1
對應的讀操作:
data <- ch
當我們不再使用 channel 的時候,可以對其進行關閉:
close(ch)
當 channel 被關閉后,如果繼續(xù)往里面寫數(shù)據(jù),則程序會直接 panic 退出。
不過讀取關閉后的 channel,不會產生 pannic,還是可以讀到數(shù)據(jù)。
如果關閉后的 channel 沒有數(shù)據(jù)可讀取時,將得到零值,即對應類型的默認值。
為了能知道當前 channel 是否被關閉,可以使用下面的寫法來判斷。
if v, ok := <-ch; !ok { fmt.Println("channel 已關閉,讀取不到數(shù)據(jù)") }
還可以使用下面的寫法不斷的獲取 channel 里的數(shù)據(jù):
for data := range ch { // get data dosomething }
這種用法會在讀取完 channel 里的數(shù)據(jù)后就結束 for 循環(huán),執(zhí)行后面的代碼。
channel 和 select
在寫程序時,有時并不單單只會和一個 goroutine 通信,當我們要進行多 goroutine 通信時,則會使用 select 寫法來管理多個 channel 的通信數(shù)據(jù):
ch1 := make(chan struct{}) ch2 := make(chan struct{}) // ch1, ch2 發(fā)送數(shù)據(jù) go sendCh1(ch1) go sendCh1(ch2) // channel 數(shù)據(jù)接受處理 for { select { case <-ch1: doSomething1() case <-ch2: doSomething2() } }
channel 的 deadlock
前面提到過,往 channel 里讀寫數(shù)據(jù)時是有可能被阻塞住的,一旦被阻塞,則需要其他的 goroutine 執(zhí)行對應的讀寫操作,才能解除阻塞狀態(tài)。
然而,阻塞后一直沒能發(fā)生調度行為,沒有可用的 goroutine 可執(zhí)行,則會一直卡在這個地方,程序就失去執(zhí)行意義了。此時 Go 就會報 deadlock 錯誤,如下代碼:
func main() { ch := make(chan int) <-ch // 執(zhí)行后將 panic: // fatal error: all goroutines are asleep - deadlock! }
因此,在使用 channel 時要注意 goroutine 的一發(fā)一取,避免 goroutine 永久阻塞!
三、channel 的底層原理
前面提及過 channel 創(chuàng)建后返回了 hchan 結構體,現(xiàn)在我們來研究下這個結構體,它的主要字段如下:
type hchan struct { qcount uint // channel 里的元素計數(shù) dataqsiz uint // 可以緩沖的數(shù)量,如 ch := make(chan int, 10)。 此處的 10 即 dataqsiz elemsize uint16 // 要發(fā)送或接收的數(shù)據(jù)類型大小 buf unsafe.Pointer // 當 channel 設置了緩沖數(shù)量時,該 buf 指向一個存儲緩沖數(shù)據(jù)的區(qū)域,該區(qū)域是一個循環(huán)隊列的數(shù)據(jù)結構 closed uint32 // 關閉狀態(tài) sendx uint // 當 channel 設置了緩沖數(shù)量時,數(shù)據(jù)區(qū)域即循環(huán)隊列此時已發(fā)送數(shù)據(jù)的索引位置 recvx uint // 當 channel 設置了緩沖數(shù)量時,數(shù)據(jù)區(qū)域即循環(huán)隊列此時已接收數(shù)據(jù)的索引位置 recvq waitq // 想讀取數(shù)據(jù)但又被阻塞住的 goroutine 隊列 sendq waitq // 想發(fā)送數(shù)據(jù)但又被阻塞住的 goroutine 隊列 lock mutex ... }
channel 在進行讀寫數(shù)據(jù)時,會根據(jù)無緩沖、有緩沖設置進行對應的阻塞喚起動作,它們之間還是有區(qū)別的。下面我們來捋一下這些不同之處。
無緩沖 channel
由于對 channel 的讀寫先后順序不同,處理也會有所不同,所以,還得再進一步區(qū)分:
channel 先寫再讀
在這里,我們暫時認為有 2 個 goroutine 在使用 channel 通信,按先寫再讀的順序,則具體流程如下:
可以看到,由于 channel 是無緩沖的,所以 G1 暫時被掛在 sendq 隊列里,然后 G1 調用了 gopark 休眠了起來。
接著,又有 goroutine 來 channel 讀取數(shù)據(jù)了:
此時 G2 發(fā)現(xiàn) sendq 等待隊列里有 goroutine 存在,于是直接從 G1 copy 數(shù)據(jù)過來,并且會對 G1 設置 goready 函數(shù),這樣下次調度發(fā)生時, G1 就可以繼續(xù)運行,并且會從等待隊列里移除掉。
channel 先讀再寫
先讀再寫的流程跟上面一樣。
G1 暫時被掛在了 recvq 隊列,然后休眠起來。
G2 在寫數(shù)據(jù)時,發(fā)現(xiàn) recvq 隊列有 goroutine 存在,于是直接將數(shù)據(jù)發(fā)送給 G1。同時設置 G1 goready 函數(shù),等待下次調度運行。
有緩沖 channel
在分析完了無緩沖 channel 的讀寫后,我們繼續(xù)看看有緩沖 channel 的讀寫。同樣的,我們分為 2 種情況:
channel 先寫再讀
這一次會優(yōu)先判斷緩沖數(shù)據(jù)區(qū)域是否已滿,如果未滿,則將數(shù)據(jù)保存在緩沖數(shù)據(jù)區(qū)域,即環(huán)形隊列里。如果已滿,則和之前的流程是一樣的。
當 G2 要讀取數(shù)據(jù)時,會優(yōu)先從緩沖數(shù)據(jù)區(qū)域去讀取,并且在讀取完后,會檢查 sendq 隊列,如果 goroutine 有等待隊列,則會將它上面的 data 補充到緩沖數(shù)據(jù)區(qū)域,并且也對其設置 goready 函數(shù)。
channel 先讀再寫
此種情況和無緩沖的先讀再寫是一樣流程,此處不再重復說明。
總結
有緩沖 channel 和無緩沖 channel 的讀寫基本相差不大,只是多了緩沖數(shù)據(jù)區(qū)域的判斷而已。
channel 在使用的時候大多時候得和 select 配合使用,盡管只需要簡單的用 <- ch 和 ch <- 來讀寫數(shù)據(jù),但它的底層還是很有講究的,特別是涉及到調度的休眠喚起。
這也能看出 Go 的精妙之處:復雜底層,優(yōu)雅運用。
以上就是golang channel多協(xié)程通信常用方法底層原理全面解析的詳細內容,更多關于golang channel多協(xié)程通信的資料請關注腳本之家其它相關文章!
相關文章
PHP和GO對接ChatGPT實現(xiàn)聊天機器人效果實例
這篇文章主要為大家介紹了PHP和GO對接ChatGPT實現(xiàn)聊天機器人效果實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01golang實現(xiàn)并發(fā)數(shù)控制的方法
下面小編就為大家分享一篇golang實現(xiàn)并發(fā)數(shù)控制的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-12-12go語言實現(xiàn)Elasticsearches批量修改查詢及發(fā)送MQ操作示例
這篇文章主要為大家介紹了go語言實現(xiàn)Elasticsearches批量修改查詢及發(fā)送MQ操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-04-04