Go 語言 基于select的多路復(fù)用

2023-03-14 16:57 更新

原文鏈接:https://gopl-zh.github.io/ch8/ch8-07.html


8.7. 基于select的多路復(fù)用

下面的程序會進(jìn)行火箭發(fā)射的倒計(jì)時(shí)。time.Tick函數(shù)返回一個(gè)channel,程序會周期性地像一個(gè)節(jié)拍器一樣向這個(gè)channel發(fā)送事件。每一個(gè)事件的值是一個(gè)時(shí)間戳,不過更有意思的是其傳送方式。

gopl.io/ch8/countdown1

func main() {
    fmt.Println("Commencing countdown.")
    tick := time.Tick(1 * time.Second)
    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)
        <-tick
    }
    launch()
}

現(xiàn)在我們讓這個(gè)程序支持在倒計(jì)時(shí)中,用戶按下return鍵時(shí)直接中斷發(fā)射流程。首先,我們啟動(dòng)一個(gè)goroutine,這個(gè)goroutine會嘗試從標(biāo)準(zhǔn)輸入中讀入一個(gè)單獨(dú)的byte并且,如果成功了,會向名為abort的channel發(fā)送一個(gè)值。

gopl.io/ch8/countdown2

abort := make(chan struct{})
go func() {
    os.Stdin.Read(make([]byte, 1)) // read a single byte
    abort <- struct{}{}
}()

現(xiàn)在每一次計(jì)數(shù)循環(huán)的迭代都需要等待兩個(gè)channel中的其中一個(gè)返回事件了:當(dāng)一切正常時(shí)的ticker channel(就像NASA jorgon的"nominal",譯注:這梗估計(jì)我們是不懂了)或者異常時(shí)返回的abort事件。我們無法做到從每一個(gè)channel中接收信息,如果我們這么做的話,如果第一個(gè)channel中沒有事件發(fā)過來那么程序就會立刻被阻塞,這樣我們就無法收到第二個(gè)channel中發(fā)過來的事件。這時(shí)候我們需要多路復(fù)用(multiplex)這些操作了,為了能夠多路復(fù)用,我們使用了select語句。

select {
case <-ch1:
    // ...
case x := <-ch2:
    // ...use x...
case ch3 <- y:
    // ...
default:
    // ...
}

上面是select語句的一般形式。和switch語句稍微有點(diǎn)相似,也會有幾個(gè)case和最后的default選擇分支。每一個(gè)case代表一個(gè)通信操作(在某個(gè)channel上進(jìn)行發(fā)送或者接收),并且會包含一些語句組成的一個(gè)語句塊。一個(gè)接收表達(dá)式可能只包含接收表達(dá)式自身(譯注:不把接收到的值賦值給變量什么的),就像上面的第一個(gè)case,或者包含在一個(gè)簡短的變量聲明中,像第二個(gè)case里一樣;第二種形式讓你能夠引用接收到的值。

select會等待case中有能夠執(zhí)行的case時(shí)去執(zhí)行。當(dāng)條件滿足時(shí),select才會去通信并執(zhí)行case之后的語句;這時(shí)候其它通信是不會執(zhí)行的。一個(gè)沒有任何case的select語句寫作select{},會永遠(yuǎn)地等待下去。

讓我們回到我們的火箭發(fā)射程序。time.After函數(shù)會立即返回一個(gè)channel,并起一個(gè)新的goroutine在經(jīng)過特定的時(shí)間后向該channel發(fā)送一個(gè)獨(dú)立的值。下面的select語句會一直等待直到兩個(gè)事件中的一個(gè)到達(dá),無論是abort事件或者一個(gè)10秒經(jīng)過的事件。如果10秒經(jīng)過了還沒有abort事件進(jìn)入,那么火箭就會發(fā)射。

func main() {
    // ...create abort channel...

    fmt.Println("Commencing countdown.  Press return to abort.")
    select {
    case <-time.After(10 * time.Second):
        // Do nothing.
    case <-abort:
        fmt.Println("Launch aborted!")
        return
    }
    launch()
}

下面這個(gè)例子更微妙。ch這個(gè)channel的buffer大小是1,所以會交替的為空或?yàn)闈M,所以只有一個(gè)case可以進(jìn)行下去,無論i是奇數(shù)或者偶數(shù),它都會打印0 2 4 6 8。

ch := make(chan int, 1)
for i := 0; i < 10; i++ {
    select {
    case x := <-ch:
        fmt.Println(x) // "0" "2" "4" "6" "8"
    case ch <- i:
    }
}

如果多個(gè)case同時(shí)就緒時(shí),select會隨機(jī)地選擇一個(gè)執(zhí)行,這樣來保證每一個(gè)channel都有平等的被select的機(jī)會。增加前一個(gè)例子的buffer大小會使其輸出變得不確定,因?yàn)楫?dāng)buffer既不為滿也不為空時(shí),select語句的執(zhí)行情況就像是拋硬幣的行為一樣是隨機(jī)的。

下面讓我們的發(fā)射程序打印倒計(jì)時(shí)。這里的select語句會使每次循環(huán)迭代等待一秒來執(zhí)行退出操作。

gopl.io/ch8/countdown3

func main() {
    // ...create abort channel...

    fmt.Println("Commencing countdown.  Press return to abort.")
    tick := time.Tick(1 * time.Second)
    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)
        select {
        case <-tick:
            // Do nothing.
        case <-abort:
            fmt.Println("Launch aborted!")
            return
        }
    }
    launch()
}

time.Tick函數(shù)表現(xiàn)得好像它創(chuàng)建了一個(gè)在循環(huán)中調(diào)用time.Sleep的goroutine,每次被喚醒時(shí)發(fā)送一個(gè)事件。當(dāng)countdown函數(shù)返回時(shí),它會停止從tick中接收事件,但是ticker這個(gè)goroutine還依然存活,繼續(xù)徒勞地嘗試向channel中發(fā)送值,然而這時(shí)候已經(jīng)沒有其它的goroutine會從該channel中接收值了——這被稱為goroutine泄露(§8.4.4)。

Tick函數(shù)挺方便,但是只有當(dāng)程序整個(gè)生命周期都需要這個(gè)時(shí)間時(shí)我們使用它才比較合適。否則的話,我們應(yīng)該使用下面的這種模式:

ticker := time.NewTicker(1 * time.Second)
<-ticker.C    // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate

有時(shí)候我們希望能夠從channel中發(fā)送或者接收值,并避免因?yàn)榘l(fā)送或者接收導(dǎo)致的阻塞,尤其是當(dāng)channel沒有準(zhǔn)備好寫或者讀時(shí)。select語句就可以實(shí)現(xiàn)這樣的功能。select會有一個(gè)default來設(shè)置當(dāng)其它的操作都不能夠馬上被處理時(shí)程序需要執(zhí)行哪些邏輯。

下面的select語句會在abort channel中有值時(shí),從其中接收值;無值時(shí)什么都不做。這是一個(gè)非阻塞的接收操作;反復(fù)地做這樣的操作叫做“輪詢channel”。

select {
case <-abort:
    fmt.Printf("Launch aborted!\n")
    return
default:
    // do nothing
}

channel的零值是nil。也許會讓你覺得比較奇怪,nil的channel有時(shí)候也是有一些用處的。因?yàn)閷σ粋€(gè)nil的channel發(fā)送和接收操作會永遠(yuǎn)阻塞,在select語句中操作nil的channel永遠(yuǎn)都不會被select到。

這使得我們可以用nil來激活或者禁用case,來達(dá)成處理其它輸入或輸出事件時(shí)超時(shí)和取消的邏輯。我們會在下一節(jié)中看到一個(gè)例子。

練習(xí) 8.8: 使用select來改造8.3節(jié)中的echo服務(wù)器,為其增加超時(shí),這樣服務(wù)器可以在客戶端10秒中沒有任何喊話時(shí)自動(dòng)斷開連接。



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號