Go select

通過select語句可以監(jiān)聽channel上的數(shù)據(jù)流動

Golang的select語句類似于UNIX的select()函數(shù)的輪詢機制吃警,在UNIX中可通過調(diào)用select()函數(shù)來監(jiān)控一系列的文件句柄(文件描述符姓言,數(shù)量有限)境蜕,一旦某個文件句柄發(fā)生了I/O動作select()調(diào)用就會被返回。該機制也被用于高并發(fā)的Socket服務器程序中则吟。

select來源于網(wǎng)絡I/O模型中的select伙狐,本質上I/O多路復用技術抒钱,只不過Golang中的select基于的并非網(wǎng)絡而是channel

select {
  case communication clause:
    statement(s)
  case communication clause:
    statement(s)
  default:
    statement(s)
}

select語句是Golang中的控制結構鼎天,類似用于通信的switch語句舀奶,也被稱為channel開關。select語句會等待channel準備就緒(收發(fā)操作)斋射,以便在不同的case下執(zhí)行育勺。由select開始的一個新的選擇塊,每個選擇條件由case語句來描述罗岖。與switch語句相比涧至,select有較多限制,最大的限制在于每個case語句必須是一個I/O操作桑包。

select{
case v1 := <-ch1:
    fmt.Printf("[CH1] received: %v\n", v1)
case v2,ok := <-ch2:
    if ok{
        fmt.Printf("[CH2] received: %v\n", v2)
    }else{
        fmt.Printf("[CH2] closed\n")
    }
case ch3 <-msg:
    fmt.Printf("[CH3] sent: %v\n", msg)
default:
    fmt.Printf("NO Communicating\n")
}

例如:上例select語句擁有四個case子句化借,前兩個是channelreceive接收操作,第三個是channelsend發(fā)送操作捡多,最后一個default默認操作蓖康。當代碼執(zhí)行到select語句時,case子句會按源代碼的順序進行評估垒手,且只評估一次蒜焊。評估結果會出現(xiàn)下面幾種情況:

  1. default外,若只有一個case評估通過則執(zhí)行此case中的語句科贬。
  2. default外泳梆,若用多個case評估通過則隨機挑選一個執(zhí)行。
  3. default外榜掌,所有的case評估都不通過优妙,則執(zhí)行default
  4. 若沒有defaultselect代碼塊發(fā)生阻塞憎账,直到有一個case通過評估套硼,否則一直阻塞。
  5. casereceive接收的是nil則也會發(fā)生阻塞

監(jiān)聽

Golang的select用來監(jiān)聽和channel有關的I/O操作胞皱,可監(jiān)聽進入channel時的數(shù)據(jù)邪意,也可以是用channel發(fā)送值時九妈。當I/O操作發(fā)生時會觸發(fā)相應地動作,因此每個case都必須是一個I/O操作雾鬼,確切的說應該是一個面向channel的I/O操作萌朱。

例如:定時器

ticker1 := time.NewTicker(time.Second * 1)
ticker2 := time.NewTicker(time.Second * 3)

for{
    select{
    case <-ticker1.C:
        fmt.Printf("[1] TICK\n")
    case <-ticker2.C:
        fmt.Printf("[2] TICK\n")
    }
}

在執(zhí)行select語句時,運行時系統(tǒng)會自上而下地判斷每個case中的發(fā)送或接收操作是否可以被立即執(zhí)行策菜,所謂立即執(zhí)行即當前goroutine不會因此操作而被阻塞晶疼。

select語句只能用于channel的讀寫操作

例如:使用select來檢測channel是否已滿

ch := make(chan int, 1)
ch<-1

select{
case ch<-2:
    fmt.Printf("send to channel\n")
default:
    fmt.Printf("channel is full\n")
}

特性

  • 每個case都必須是一個channel
  • 所有channel表達式都會被求值
  • 所有被發(fā)送的表達式都會被求值
  • 若任意某個channel可執(zhí)行就執(zhí)行,其它被忽略又憨。

例如:獲取斐波拉茲數(shù)列

func fib(ch, quit chan int){
    x,y := 0,1
    for{
        select{
        case ch <- x:
            x,y = y, x+y
        case <-quit:
            fmt.Printf("QUIT\n")
            return
        }
    }
}

func main(){
    ch := make(chan int)
    quit := make(chan int)

    go func(){
        for i:=0; i<10; i++{
            fmt.Println(<-ch)
        }
        quit<-0
    }()

    fib(ch, quit)
}

case

  • select中每個case必須是一個channel操作冒晰,要么是發(fā)送要么是接收。

select執(zhí)行過程中必須命中某一case分支竟块,若在遍歷所有case后都沒有命中壶运,則會進入default分支。若沒有default分支則select發(fā)生阻塞浪秘,直到某個case可以命中蒋情。若一直都沒有命中,則select拋出deadlock死鎖錯誤耸携。

  • 循環(huán)中每次select都會對所有channel表達式求值

例如:通過time.After實現(xiàn)定時器棵癣,定時任務可通過done channel停止。

done := make(chan bool, 1)
close(done)
for{
    select{
    case <-time.After(time.Second):
        fmt.Printf("Time after\n")
    case <-done:
        //讀取零值 false
        fmt.Printf("Read done\n")
    }
}
  • 若多個case滿足讀寫條件夺衍,select會隨機選擇一個case來執(zhí)行狈谊。

select會隨機執(zhí)行一個可運行的case,若沒有case可以運行則阻塞沟沙,直到有case可以運行河劝。

select可用于多個channel進行讀寫操作時僅需一次只處理一個的情況。

ch := make(chan int, 1024)
go func(ch chan int){
    for{
        v := <-ch
        fmt.Printf("value = %v\n", v)
    }
}(ch)

ticker := time.NewTicker(time.Second * 1)
for i:=0; i<5; i++{
    select{
    case ch<-i:
    case <-ticker.C:
        fmt.Printf("%d: Ticker\n", i)
    }
    time.Sleep(time.Microsecond * 500)
}

close(ch)
ticker.Stop()

ticker.Cch同時滿足讀寫條件時矛紫,select會隨機地選擇一個來執(zhí)行赎瞎,導致看起來一些數(shù)據(jù)丟了。

  • 對于case條件語句中若存在channel值為nil的讀寫操作颊咬,則該分支會被忽略务甥。
var ch chan int

go func(ch chan int){
    ch <- 100
}(ch)

select {
case <-ch:
    fmt.Printf("Channel recieved\n")
}

發(fā)生錯誤:fatal error: all goroutines are asleep - deadlock!

default

select語句會被阻塞,直到其中一個case被執(zhí)行喳篇。若select中沒有任何case敞临,它將永遠阻塞,從而導致死鎖麸澜。

例如:空select{}引發(fā)死鎖

func main(){
    select {
    
    }
}

對于空的select語句挺尿,程序會被阻塞,準確來說是當前goroutine會被阻塞。Golang自帶死鎖檢測機制票髓,發(fā)現(xiàn)當前goroutine再也沒有機會被喚醒時,則會panic铣耘。

fatal error: all goroutines are asleep - deadlock!

通過帶defaultselect實現(xiàn)非阻塞讀寫洽沟,用于防止select發(fā)生阻塞。

select{
  default:
}

多個case運行時select會隨機公平地選出一個執(zhí)行蜗细,其它不會執(zhí)行裆操。若存在default子句則會執(zhí)行該語句。若沒有defaultselect阻塞炉媒,直到某個通信可以運行踪区。Go不會重新對channel或值進行求值。

select語句永遠阻塞吊骤,沒有其它goroutine寫入此channel時缎岗,將導致死鎖。

ch := make(chan int)
select{
case <-ch:
}
fatal error: all goroutines are asleep - deadlock!

若存在默認情況default則不會發(fā)生死鎖deadlock白粉,因為在沒有其它case準備就緒時將執(zhí)行default默認情況传泊。

ch := make(chan int)
select{
case <-ch:
default:
    fmt.Println("default case executed")
}

例如:典型生產(chǎn)者消費者模式

func main(){
    ch1 := make(chan int)
    ch2 := make(chan int)
    //生產(chǎn)者
    go pump1(ch1)
    go pump2(ch2)
    //消費者
    go suck(ch1, ch2)

    time.Sleep(1e9)
}
  • ch1ch2在無限循環(huán)中通過pump1()pump2()填充整數(shù)
func pump1(ch chan int){
    for i:=0; ; i++{
        ch <- i * 1
    }
}
func pump2(ch chan int){
    for i:=0; ; i++{
        ch <- i * 2
    }
}
  • suck()在無限循環(huán)中輪詢輸入項,通過select語句獲取不同信道的整數(shù)并輸出鸭巴。
func suck(ch1,ch2 chan int){
    for{
        select{
        case v := <-ch1:
            fmt.Printf("[CH1] receive %d\n", v)
        case v := <-ch2:
            fmt.Printf("[CH2] receive %d\n", v)
        default:
            fmt.Printf("NO Communicating\n")
        }
    }
}

選擇select的哪一個case取決于哪個信道接收到了消息眷细。

timeout

case中的channel始終沒有接收到數(shù)據(jù),同時也沒有提供default語句時鹃祖,select語句整體會發(fā)生阻塞溪椎。有時并不希望select一直阻塞下去,此時可手動設置一個超時時間恬口。

func expire(ch chan bool, t int){
    time.Sleep(time.Second * time.Duration(t))
    ch <- true
}

func main(){
    timeout := make(chan bool, 1)
    go expire(timeout, 2)

    ch1 := make(chan string, 1)
    ch2 := make(chan string, 1)
    select{
    case msg1 := <-ch1:
        fmt.Printf("[CH1] received: %s\n", msg1)
    case msg2 := <-ch2:
        fmt.Printf("[CH2] received: %s\n", msg2)
    case <-timeout:
        fmt.Printf("[EXPIRE] exit\n")
    }
}

例如:使用select實現(xiàn)channel的讀取超時機制校读,不能使用default否則3秒超時未到,就會直接執(zhí)行default祖能。

timeout := make(chan bool, 1)

go func(){
    time.Sleep(time.Second * 3)
    timeout <- true
}()

ch := make(chan int)
select{
case <-ch:
case <-timeout:
    fmt.Printf("TIMEOUT\n")
}

可使用time.After實現(xiàn)超時控制

ch := make(chan int)

select{
case <-ch:
    fmt.Printf("read from ch\n")
case <-time.After(time.Second * time.Duration(3)):
    fmt.Printf("[TIMEOUT] exit\n")
}

for

forselect結合時地熄,break是無法跳出for之外的,若需break出來需添加標簽使用goto芯杀,或break到具體為止端考。

  • 解決方案1:使用Golang中break的特性在外層for上添加一個標簽
  • 解決方案2:使用goto直接跳出循環(huán)到指定標記位置
  • 對于for中空的select{}也有可能會引起CPU占用過高的問題
ch := make(chan bool)
for i:=0; i<runtime.NumCPU(); i++{
    go func(){
        for{
            select{
            case <-ch:
                break
            default:
            }
        }
    }()
}

time.Sleep(time.Second * 10)
for i:=0; i<runtime.NumCPU(); i++{
    ch<-true
}

一般來說,使用select監(jiān)聽各個case的I/O事件揭厚,每個case都是阻塞的却特。上例中原本希望select在獲取到ch里的數(shù)據(jù)時立即退出循環(huán),但由于在for循環(huán)中筛圆,第一次讀取ch后僅僅退出了select但并未退出for裂明,因此下次哈希繼續(xù)執(zhí)行select邏輯,此時將永遠是執(zhí)行default太援,直到ch里讀取到數(shù)據(jù)闽晦。否則會一直在一個死循環(huán)中運行扳碍,因此即便只是放到一個goroutine中運行,也會占滿所有的CPU仙蛉。解決的方式直接把default拿掉笋敞,這樣select會一直阻塞在ch通道的I/O上,當ch有數(shù)據(jù)時就可以隨時響應通道中的信息荠瘪。

select實現(xiàn)了一種監(jiān)聽模式夯巷,通常用在(無限)循環(huán)中,在某中情況下可通過break語句使循環(huán)退出哀墓。

ch := make(chan int)
//定時2s
ticker := time.NewTicker(time.Second * 2)
defer ticker.Stop()
//發(fā)送信號
go func(ch chan int){
    time.Sleep(time.Second * 5)
    ch <- 1
}(ch)
//監(jiān)聽I/O
for{
    select{
    case <-ticker.C:
        fmt.Printf("task running...\n")
    case result,ok := <-ch:{
        if ok{
            fmt.Printf("chan number is %v\n", result)
            break
        }
    }
    }
}
fmt.Printf("END\n")

例如:

ch := make(chan int)
quit := make(chan bool)

//寫數(shù)據(jù)
go func() {
    //循環(huán)寫入
    for i := 0; i < 5; i++ {
        ch <- i
        time.Sleep(time.Second)
    }
    //關閉channel
    close(ch)
    //通知主goroutine推出
    quit <- true
    //退出當前goroutine
    runtime.Goexit()
}()

//主goroutine 讀數(shù)據(jù)
for {
    select {
    case num := <-ch:
        fmt.Printf("received number is %d\n", num)
    case <-quit:
        fmt.Printf("quit\n")
        //break //跳出select
        return //終止進程
    }
    fmt.Printf("==================\n")
}

多路復用

select是Golang在語言層面提供的多路I/O復用機制趁餐,它可檢測多個channel是否ready(是否可讀或可寫)。

select是如何實現(xiàn)多路復用的篮绰,為什么沒有在第一個channel操作時阻塞后雷,從而導致后面的case都執(zhí)行不了。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吠各,一起剝皮案震驚了整個濱河市喷面,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌走孽,老刑警劉巖惧辈,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異磕瓷,居然都是意外死亡盒齿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門困食,熙熙樓的掌柜王于貴愁眉苦臉地迎上來边翁,“玉大人,你說我怎么就攤上這事硕盹》遥” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵瘩例,是天一觀的道長啊胶。 經(jīng)常有香客問我,道長垛贤,這世上最難降的妖魔是什么焰坪? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮聘惦,結果婚禮上某饰,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好黔漂,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布诫尽。 她就那樣靜靜地躺著,像睡著了一般炬守。 火紅的嫁衣襯著肌膚如雪牧嫉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天劳较,我揣著相機與錄音驹止,去河邊找鬼浩聋。 笑死观蜗,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的衣洁。 我是一名探鬼主播墓捻,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坊夫!你這毒婦竟也來了砖第?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤环凿,失蹤者是張志新(化名)和其女友劉穎梧兼,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體智听,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡羽杰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了到推。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片考赛。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖莉测,靈堂內(nèi)的尸體忽然破棺而出颜骤,到底是詐尸還是另有隱情,我是刑警寧澤捣卤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布忍抽,位于F島的核電站,受9級特大地震影響董朝,放射性物質發(fā)生泄漏梯找。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一益涧、第九天 我趴在偏房一處隱蔽的房頂上張望锈锤。 院中可真熱鬧,春花似錦、人聲如沸久免。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阎姥。三九已至记舆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呼巴,已是汗流浹背泽腮。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留衣赶,地道東北人诊赊。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像府瞄,于是被迫代替她去往敵國和親碧磅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Go 的select語句是一種僅能用于channl發(fā)送和接收消息的專用語句遵馆,此語句運行期間是阻塞的鲸郊;當select...
    菜剛RyuGou閱讀 1,376評論 0 12
  • golang 的 select 的功能和 select, poll, epoll 相似, 就是監(jiān)聽 IO 操作货邓,當...
    builderfa閱讀 484評論 0 0
  • select 語句的行為 select就是用來監(jiān)聽和channel有關的IO操作 上面這段代碼中秆撮,select 語...
    Jaycee88閱讀 265評論 1 0
  • Go里面的一個關鍵字,用于監(jiān)聽channel上的數(shù)據(jù)流動换况。select語句里面的每條case語句必須是一個IO操作...
    騎蝸上高速閱讀 288評論 0 0
  • 下面的例子很好的反映了select复隆、case拨匆、協(xié)程的運作,實現(xiàn)的功能是:隨機打印1或者2挽拂,到達超時后停止惭每。 實現(xiàn)方...
    JoyHair閱讀 3,522評論 0 1