golang踩坑實(shí)戰(zhàn)之channel的正確使用方式
一、為什么要用channel
筆者也是從Java轉(zhuǎn)Go的選手,之前一直很難擺脫線程池、可重入鎖、AQS等數(shù)據(jù)結(jié)構(gòu)及其底層的思維定式。而最近筆者也開(kāi)始逐漸回顧過(guò)往的實(shí)習(xí)和實(shí)驗(yàn),慢慢領(lǐng)悟了golang并發(fā)的一些經(jīng)驗(yàn)了。
golang在解決并發(fā)race問(wèn)題時(shí),首要考慮的方案是使用channel??赡芎芏嗳藭?huì)喜歡用互斥鎖sync.Mutex,因?yàn)閙utex lock只有Lock和Unlock兩種操作,與Java中的ReentrantLock比較類(lèi)似。但筆者實(shí)踐過(guò)程中發(fā)現(xiàn):
互斥鎖只能做到阻塞,而無(wú)法讓流程之間通信。如果不同流程之間需要交流,則需要一個(gè)類(lèi)似于信號(hào)量一樣的機(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)然并不是說(shuō)channel可完全替代鎖,鎖可以使得代碼和邏輯更簡(jiǎn)單)
二、基本操作
2.1 channel
channel可以看作一個(gè)FIFO的隊(duì)列,隊(duì)列進(jìn)出都是原子操作。隊(duì)列內(nèi)部元素的類(lèi)型可以自由選擇。以下給出channel的常見(jiàn)操作
//初始化
ss := make(chan struct{})
sb := make(chan bool)
var s chan bool
si = make(chan int)
// 寫(xiě)
si <- 1
sb <- true
ss <- struct{}
//讀
<-sb
i := <-si
fmt.Print(i+1)//2
// 使用完畢的channel可close
close(si)2.2 channel緩存
一般來(lái)說(shuō),channel有帶緩存和不帶緩存兩種。
不帶緩存的channel讀和寫(xiě)都是阻塞的,一旦某個(gè)channel發(fā)生寫(xiě)操作,除非另一個(gè)goroutine使用讀操作將元素從channel取出,否則當(dāng)前goroutine會(huì)一直阻塞。反之,如果一個(gè)不帶緩存的channel被一個(gè)goroutine讀取,除非另一個(gè)goroutine對(duì)該channel發(fā)起寫(xiě)入,否則當(dāng)前goroutine會(huì)一直被阻塞。
下面這個(gè)單元測(cè)試的結(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í)可以寫(xiě)入數(shù)據(jù)后直接返回,緩存中有數(shù)據(jù)時(shí)可以直接讀出。如果緩存空間寫(xiě)滿,同時(shí)沒(méi)有被讀取,那寫(xiě)入會(huì)阻塞。同理,如果緩存空間沒(méi)有數(shù)據(jù),讀入也會(huì)阻塞,直到有數(shù)據(jù)被寫(xiě)入。
//會(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ù)寫(xiě)入
func TestChannel3(t *testing.T) {
c := make(chan int,1)
c<-1
c<-1
}
//會(huì)死鎖,因?yàn)橐恢弊x取阻塞,沒(méi)有寫(xiě)入
func TestChannel4(t *testing.T) {
c := make(chan int,1)
<-c
}
2.3 只讀只寫(xiě)channel
有些channel可以被定義為只能用于寫(xiě)入,或者只能用于發(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對(duì)多個(gè)channel操作進(jìn)行同時(shí)監(jiān)聽(tīng),當(dāng)某個(gè)case子句可以運(yùn)行時(shí),該case下面的邏輯會(huì)執(zhí)行,且select語(yǔ)句結(jié)束。如果定義了default語(yǔ)句,且各個(gè)case中的執(zhí)行均被阻塞無(wú)法完成時(shí),程序便會(huì)進(jìn)入default的邏輯中。
值得注意的是,如果有多個(gè)case可以滿足,最終執(zhí)行的case語(yǔ)句是不確定的(不同于switch語(yǔ)句的從上到下依次判斷是否滿足)。
下面用一個(gè)例子來(lái)說(shuō)明
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都無(wú)數(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
對(duì)channel的for range循環(huán)可以依次從channel中讀取數(shù)據(jù),讀取數(shù)據(jù)前是不知道里面有多少元素的,如果channel中沒(méi)有元素,則會(huì)阻塞等待,直到channel被關(guān)閉,退出循環(huán)。如果代碼中沒(méi)有關(guān)閉channel的邏輯,或者插入break語(yǔ)句的話,就會(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輪詢過(guò)的對(duì)象可以被視為已經(jīng)從channel取出,下面我們拿兩個(gè)例子來(lái)說(shuō)明:
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è)核心用法就是流程控制,對(duì)于狀態(tài)機(jī)輪轉(zhuǎn)場(chǎng)景,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)聽(tīng)該channel,一旦被寫(xiě)入,即可進(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ù)對(duì)應(yīng)的channel寫(xiě)入,由select讀出。同時(shí)用一個(gè)case來(lái)計(jì)時(shí),如果超過(guò)該時(shí)間仍然沒(méi)有完成,則進(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后才寫(xiě)入channel
taskAChan <- true
}
3.4 帶并發(fā)數(shù)限制的goroutine池
我實(shí)習(xí)的時(shí)候曾經(jīng)碰到一個(gè)需求,需要并發(fā)地向目標(biāo)服務(wù)器發(fā)起ftp請(qǐng)求,但是同一時(shí)間能發(fā)起的連接數(shù)量是有限的,需要由buffer channel對(duì)其進(jìn)行控制。該channel有點(diǎn)類(lèi)似于信號(hào)量,讀取寫(xiě)入會(huì)導(dǎo)致緩存空間的變化。緩存在這里起的作用類(lèi)似于信號(hào)量(寫(xiě)入讀取對(duì)應(yīng)PV操作),進(jìn)行任務(wù)時(shí)會(huì)寫(xiě)入channel,完成任務(wù)時(shí)會(huì)讀取channel。如果緩存空間耗盡,就會(huì)新的寫(xiě)入請(qǐng)求會(huì)阻塞,直到某一個(gè)任務(wù)完成緩存空間釋放。
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // 等待放行;
process(r)
// 可能需要一個(gè)很長(zhǎng)的處理過(guò)程;
<-sem // 完成,放行另一個(gè)過(guò)程。
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // 無(wú)需等待 handle 完成。
}
}
3.5 溢出緩存
在高并發(fā)環(huán)境下,為了避免請(qǐng)求丟失,可以選擇將來(lái)不及處理的請(qǐng)求緩存。這也是使用select可以實(shí)現(xiàn)的功能,如果一個(gè)buffer channel寫(xiě)滿,在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
- 對(duì)nil channel進(jìn)行close
- 對(duì)closed channel進(jìn)行close和寫(xiě)(讀會(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 對(duì)reset channel進(jìn)行寫(xiě)入
如果一個(gè)結(jié)構(gòu)體的channel成員有機(jī)會(huì)被重置,它的寫(xiě)入必須考慮失敗。
下面例子中,寫(xiě)入跳轉(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang通過(guò)小程序獲取微信openid的方法示例
這篇文章主要介紹了Golang通過(guò)小程序獲取微信openid的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問(wèn)題
這篇文章主要介紹了完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03
Golang中omitempty關(guān)鍵字的具體實(shí)現(xiàn)
本文主要介紹了Golang中omitempty關(guān)鍵字的具體實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
golang實(shí)現(xiàn)圖像驗(yàn)證碼的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用golang實(shí)現(xiàn)簡(jiǎn)單的圖像驗(yàn)證碼,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-10-10
Golang中的archive/zip包的常用函數(shù)詳解
Golang 中的 archive/zip 包用于處理 ZIP 格式的壓縮文件,提供了一系列用于創(chuàng)建、讀取和解壓縮 ZIP 格式文件的函數(shù)和類(lèi)型,下面小編就來(lái)和大家講解下常用函數(shù)吧2023-08-08
為什么Go里值為nil可以調(diào)用函數(shù)原理分析
這篇文章主要為大家介紹了為什么Go里值為nil可以調(diào)用函數(shù)原理分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
golang?gorm的Callbacks事務(wù)回滾對(duì)象操作示例
這篇文章主要為大家介紹了golang?gorm的Callbacks事務(wù)回滾對(duì)象操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04

