golang踩坑實(shí)戰(zhàn)之channel的正確使用方式
一、為什么要用channel
筆者也是從Java轉(zhuǎn)Go的選手,之前一直很難擺脫線程池、可重入鎖、AQS等數(shù)據(jù)結(jié)構(gòu)及其底層的思維定式。而最近筆者也開始逐漸回顧過往的實(shí)習(xí)和實(shí)驗(yàn),慢慢領(lǐng)悟了golang并發(fā)的一些經(jīng)驗(yàn)了。
golang在解決并發(fā)race問題時(shí),首要考慮的方案是使用channel。可能很多人會(huì)喜歡用互斥鎖sync.Mutex,因?yàn)閙utex lock只有Lock和Unlock兩種操作,與Java中的ReentrantLock比較類似。但筆者實(shí)踐過程中發(fā)現(xiàn):
互斥鎖只能做到阻塞,而無法讓流程之間通信。如果不同流程之間需要交流,則需要一個(gè)類似于信號量一樣的機(jī)制。同時(shí),最好該機(jī)制能實(shí)現(xiàn)流程控制。譬如控制不同任務(wù)執(zhí)行的先后順序,讓任務(wù)等待未完成的任務(wù),以及打斷某個(gè)輪轉(zhuǎn)的狀態(tài)。
如何實(shí)現(xiàn)這些功能?channel就是Go給出的一個(gè)優(yōu)雅的答案。(當(dāng)然并不是說channel可完全替代鎖,鎖可以使得代碼和邏輯更簡單)
二、基本操作
2.1 channel
channel可以看作一個(gè)FIFO的隊(duì)列,隊(duì)列進(jìn)出都是原子操作。隊(duì)列內(nèi)部元素的類型可以自由選擇。以下給出channel的常見操作
//初始化 ss := make(chan struct{}) sb := make(chan bool) var s chan bool si = make(chan int) // 寫 si <- 1 sb <- true ss <- struct{} //讀 <-sb i := <-si fmt.Print(i+1)//2 // 使用完畢的channel可close close(si)
2.2 channel緩存
一般來說,channel有帶緩存和不帶緩存兩種。
不帶緩存的channel讀和寫都是阻塞的,一旦某個(gè)channel發(fā)生寫操作,除非另一個(gè)goroutine使用讀操作將元素從channel取出,否則當(dāng)前goroutine會(huì)一直阻塞。反之,如果一個(gè)不帶緩存的channel被一個(gè)goroutine讀取,除非另一個(gè)goroutine對該channel發(fā)起寫入,否則當(dāng)前goroutine會(huì)一直被阻塞。
下面這個(gè)單元測試的結(jié)果是編譯器報(bào)錯(cuò),提示死鎖。
func TestChannel0(t *testing.T) { c := make(chan int) c <- 1 }
fatal error: all goroutines are asleep - deadlock!
如果要正確運(yùn)行,應(yīng)修改為
func TestChannel0(t *testing.T) { c := make(chan int) go func(c chan int) { <-c }(c) c <- 1 }
帶通道緩存的channel的特點(diǎn)是,有緩存空間時(shí)可以寫入數(shù)據(jù)后直接返回,緩存中有數(shù)據(jù)時(shí)可以直接讀出。如果緩存空間寫滿,同時(shí)沒有被讀取,那寫入會(huì)阻塞。同理,如果緩存空間沒有數(shù)據(jù),讀入也會(huì)阻塞,直到有數(shù)據(jù)被寫入。
//會(huì)成功執(zhí)行 func TestChannel1(t *testing.T) { c := make(chan int,1) go func(c chan int) { c <- 1 }(c) <-c } //不會(huì)死鎖,因?yàn)榫彺婵臻g未填滿 func TestChannel2(t *testing.T) { c := make(chan int,1) c<-1 } //會(huì)死鎖,因?yàn)榫彺婵臻g填滿后仍繼續(xù)寫入 func TestChannel3(t *testing.T) { c := make(chan int,1) c<-1 c<-1 } //會(huì)死鎖,因?yàn)橐恢弊x取阻塞,沒有寫入 func TestChannel4(t *testing.T) { c := make(chan int,1) <-c }
2.3 只讀只寫channel
有些channel可以被定義為只能用于寫入,或者只能用于發(fā)送。
下面是具體例子
func sender(c chan<- bool){ c <- true //<- c // 這一句會(huì)報(bào)錯(cuò) } func receiver(c <-chan bool){ //c <- true// 這一句會(huì)報(bào)錯(cuò) <- c } func normal(){ senderChan := make(chan<- bool) receiverChan := make(<-chan bool) }
2.4 select
select允許goroutine對多個(gè)channel操作進(jìn)行同時(shí)監(jiān)聽,當(dāng)某個(gè)case子句可以運(yùn)行時(shí),該case下面的邏輯會(huì)執(zhí)行,且select語句結(jié)束。如果定義了default語句,且各個(gè)case中的執(zhí)行均被阻塞無法完成時(shí),程序便會(huì)進(jìn)入default的邏輯中。
值得注意的是,如果有多個(gè)case可以滿足,最終執(zhí)行的case語句是不確定的(不同于switch語句的從上到下依次判斷是否滿足)。
下面用一個(gè)例子來說明
func writeTrue(c chan bool) { c <- false } // 輸出為 chan 1, 因?yàn)閏han 1有可讀數(shù)據(jù) func TestSelect0(t *testing.T) { chan1 := make(chan bool,1) chan2 := make(chan bool,1) writeTrue(chan1) select { case <-chan1: fmt.Print("chan 1") case <-chan2: fmt.Print("chan 2") default: fmt.Print("default") } } // 輸出為default, 因?yàn)閏han1和chan2都無數(shù)據(jù)可讀 func TestSelect1(t *testing.T) { chan1 := make(chan bool,1) chan2 := make(chan bool,1) select { case <-chan1: fmt.Print("chan 1") case <-chan2: fmt.Print("chan 2") default: fmt.Print("default") } } // 輸出為 chan 1或chan 2, 因?yàn)閏han 1 和chan 2均有可讀數(shù)據(jù) func TestSelect2(t *testing.T) { chan1 := make(chan bool,1) chan2 := make(chan bool,1) writeTrue(chan1) writeTrue(chan2) select { case <-chan1: fmt.Print("chan 1") case <-chan2: fmt.Print("chan 2") default: fmt.Print("default") } }
2.5 for range
對channel的for range循環(huán)可以依次從channel中讀取數(shù)據(jù),讀取數(shù)據(jù)前是不知道里面有多少元素的,如果channel中沒有元素,則會(huì)阻塞等待,直到channel被關(guān)閉,退出循環(huán)。如果代碼中沒有關(guān)閉channel的邏輯,或者插入break語句的話,就會(huì)產(chǎn)生死鎖。
func testLoopChan() { c := make(chan int) go func() { c <- 1 c <- 2 c <- 3 time.Sleep(time.Second * 2) close(c) }() for x := range c { fmt.Printf("test:%+v\n", x) } } //結(jié)果 test:1 test:2 test:3 結(jié)束
這里需要注意,被for range輪詢過的對象可以被視為已經(jīng)從channel取出,下面我們拿兩個(gè)例子來說明:
func testLoopChan2() { c := make(chan int) go func() { c <- 1 c <- 2 c <- 3 }() for x := range c { fmt.Printf("test:%+v\n", x) break } <-c <-c } //輸出 1 func testLoopChan3() { c := make(chan int) go func() { c <- 1 c <- 2 c <- 3 }() for x := range c { fmt.Printf("test:%+v\n", x) break } <-c <-c <-c } //輸出死鎖,因?yàn)閏hannel已經(jīng)取空,最后的<-操作會(huì)導(dǎo)致阻塞
三、使用
3.1 狀態(tài)機(jī)輪轉(zhuǎn)
channel的一個(gè)核心用法就是流程控制,對于狀態(tài)機(jī)輪轉(zhuǎn)場景,channel可以輕松解決(經(jīng)典的輪流打印ABC)。
func main(){ chanA :=make(chan struct{},1) chanB :=make(chan struct{},1) chanC :=make(chan struct{},1) chanA<- struct{}{} go printA(chanA,chanB) go printB(chanB,chanC) go printC(chanC,chanA) } func printA(chanA chan struct{}, chanB chan struct{}) { for { <-chanA println("A") chanB<- struct{}{} } } func printB(chanB chan struct{}, chanC chan struct{}) { for { <-chanB println("B") chanC<- struct{}{} } } func printC(chanC chan struct{}, chanA chan struct{}) { for { <-chanC println("C") chanA<- struct{}{} } }
3.2 流程退出
這是我在raft實(shí)驗(yàn)中g(shù)et到的小技能,用一個(gè)channel表示是否需要退出。select中監(jiān)聽該channel,一旦被寫入,即可進(jìn)入退出邏輯
exit := make (chan bool) //... for { select { case <-exit: fmt.Print("exit code") return default: fmt.Print("normal code") //... } }
3.3 超時(shí)控制
這也是我在raft實(shí)驗(yàn)中g(shù)et到的技能,如果某個(gè)任務(wù)返回,可以在該任務(wù)對應(yīng)的channel寫入,由select讀出。同時(shí)用一個(gè)case來計(jì)時(shí),如果超過該時(shí)間仍然沒有完成,則進(jìn)入超時(shí)邏輯
func control(){ taskAChan := make (chan bool) TaskA(taskAChan) select { case <-taskAChan: fmt.Print("taskA success") case <- <-time.After(5 * time.Second): ftm.Print("timeover") } } func TaskA(taskAChan chan bool){ //TaskA的主要代碼 //... // 完成TaskA后才寫入channel taskAChan <- true }
3.4 帶并發(fā)數(shù)限制的goroutine池
我實(shí)習(xí)的時(shí)候曾經(jīng)碰到一個(gè)需求,需要并發(fā)地向目標(biāo)服務(wù)器發(fā)起ftp請求,但是同一時(shí)間能發(fā)起的連接數(shù)量是有限的,需要由buffer channel對其進(jìn)行控制。該channel有點(diǎn)類似于信號量,讀取寫入會(huì)導(dǎo)致緩存空間的變化。緩存在這里起的作用類似于信號量(寫入讀取對應(yīng)PV操作),進(jìn)行任務(wù)時(shí)會(huì)寫入channel,完成任務(wù)時(shí)會(huì)讀取channel。如果緩存空間耗盡,就會(huì)新的寫入請求會(huì)阻塞,直到某一個(gè)任務(wù)完成緩存空間釋放。
var sem = make(chan int, MaxOutstanding) func handle(r *Request) { sem <- 1 // 等待放行; process(r) // 可能需要一個(gè)很長的處理過程; <-sem // 完成,放行另一個(gè)過程。 } func Serve(queue chan *Request) { for { req := <-queue go handle(req) // 無需等待 handle 完成。 } }
3.5 溢出緩存
在高并發(fā)環(huán)境下,為了避免請求丟失,可以選擇將來不及處理的請求緩存。這也是使用select可以實(shí)現(xiàn)的功能,如果一個(gè)buffer channel寫滿,在default邏輯中將其緩存。
func put(c message){ select { case putChannel <- c: fmt.Print("put success") default: fmt.Print("buffer data") buffer(c) } }
3.6 隨機(jī)概率分發(fā)
select { case b := <-backendMsgChan: if sampleRate > 0 && rand.Int31n(100) > sampleRate { continue } }
四、坑和經(jīng)驗(yàn)
4.1 panic
以下幾種情況會(huì)導(dǎo)致panic
- 對nil channel進(jìn)行close
- 對closed channel進(jìn)行close和寫(讀會(huì)讀出零值)
可以用ok值檢查channel是否為空或者關(guān)閉
queue := make(chan int, 1) value, ok := <-queue if !ok { fmt.Println("queue is closed or nil") queue = nil }
4.2 關(guān)閉的channel如果使用range會(huì)提前返回
channel 關(guān)閉會(huì)導(dǎo)致range返回
4.3 對reset channel進(jìn)行寫入
如果一個(gè)結(jié)構(gòu)體的channel成員有機(jī)會(huì)被重置,它的寫入必須考慮失敗。
下面例子中,寫入跳轉(zhuǎn)到了default邏輯
type chanTest struct { c chan bool } func TestResetChannel(t *testing.T) { cc := chanTest{c: make(chan bool)} go cc.resetChan() select { case cc.c <- true: log.Printf("cc.c in") default: log.Printf("default") } } func (c *chanTest) resetChan() { c.c = make(chan bool) }
總結(jié)
到此這篇關(guān)于golang踩坑實(shí)戰(zhàn)之channel的正確使用方式的文章就介紹到這了,更多相關(guān)golang channel的正確使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問題
這篇文章主要介紹了完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-03-03Golang中omitempty關(guān)鍵字的具體實(shí)現(xiàn)
本文主要介紹了Golang中omitempty關(guān)鍵字的具體實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01golang實(shí)現(xiàn)圖像驗(yàn)證碼的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用golang實(shí)現(xiàn)簡單的圖像驗(yàn)證碼,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-10-10Golang中的archive/zip包的常用函數(shù)詳解
Golang 中的 archive/zip 包用于處理 ZIP 格式的壓縮文件,提供了一系列用于創(chuàng)建、讀取和解壓縮 ZIP 格式文件的函數(shù)和類型,下面小編就來和大家講解下常用函數(shù)吧2023-08-08為什么Go里值為nil可以調(diào)用函數(shù)原理分析
這篇文章主要為大家介紹了為什么Go里值為nil可以調(diào)用函數(shù)原理分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08golang?gorm的Callbacks事務(wù)回滾對象操作示例
這篇文章主要為大家介紹了golang?gorm的Callbacks事務(wù)回滾對象操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04