1. 程式人生 > >Go基礎系列:為select設定超時時間

Go基礎系列:為select設定超時時間

After()

誰也無法保證某些情況下的select是否會永久阻塞。很多時候都需要設定一下select的超時時間,可以藉助time包的After()實現。

time.After()的定義如下:

func After(d Duration) <-chan Time

After()函式接受一個時長d,然後它After()等待d時長,等待時間到後,將等待完成時所處時間點寫入到channel中並返回這個只讀channel。

所以,將該函式賦值給一個變數時,這個變數是一個只讀channel,而channel是一個指標型別的資料,所以它是一個指標。

看下面的示例:

package main

import (
    "fmt"
    "time"
)
func main() {
    fmt.Println(time.Now())
    a := time.After(1*time.Second)
    fmt.Println(<-a)
    fmt.Println(a)
}

輸出結果:

2018-11-20 19:05:11.5440307 +0800 CST m=+0.001994801
2018-11-20 19:05:12.5496378 +0800 CST m=+1.007601901
0xc042052060

如果將After()放進select語句塊的一個case中,那麼就可以讓其它的case有一定的時間長度來監聽讀、寫事件,如果在這段時長內其它case還沒有有可讀、可寫事件,這個After()所在case就會結束當前的select,然後終止select(如果select未在迴圈中)或進入下一輪select(如果select在迴圈中)。

以下是一個示例:

func main() {
    ch1 := make(chan string)

    // 啟用一個goroutine,但5秒之後才傳送資料
    go func() {
        time.Sleep(5 * time.Second)
        ch1 <- "put value into ch1"
    }()

    select {
    case val := <-ch1:
        fmt.Println("recv value from ch1:",val)
        return
    // 只等待3秒,然後就結束
    case <-time.After(3 * time.Second):
        fmt.Println("3 second over, timeover")
    }
}

執行後,將在大約3秒之後輸出:

3 second over, timeover

上面出現了超時現象,因為新啟用的goroutine首先要等待5秒,然後才將資料傳送到channel ch1中。但是main goroutine繼續執行到select語句塊,由於第一個case未滿足條件(注意,main goroutine並不會因此而阻塞)。評估第二個case時,將執行time.After()等待3秒,3秒之後讀取到該函式返回的通道資料,於是該case滿足select的條件,該select因為沒有在迴圈中,所以直接結束,main goroutine也因此而終止。自始至終,新啟用的goroutine都沒有機會將資料傳送到ch1中。

上面有兩個注意點:

  • (1).3秒等待時,只有在等待完成時case才被選中,在等待過程中,select一直在評估所有的case右邊的表示式
  • (2).在上面的3秒等待過程中,第一個case的評估一直在持續著,因為在等待結束之前,select還未選中任何case,而是一直在評估所有的表示式,包括<-ch1的評估。

如果將上面go func()函式的睡眠時間改為2秒,則在3秒等待時間內,第一個case的<-ch1評估滿足條件,於是該case被選中,第二個case被無視。

go func() {
    time.Sleep(1 * time.Second)
    ch1 <- "put value into ch1"
}()

上面使用After(),也保證了select一定會選中某一個case,這時可以省略default塊。

注意,After()放在select的內部和放在select的外部是完全不一樣的,更助於理解的示例見下面的Tick()。

time.Tick()

After(d)是隻等待一次d的時長,並在這次等待結束後將當前時間傳送到通道。Tick(d)則是間隔地多次等待,每次等待d時長,並在每次間隔結束的時候將當前時間傳送到通道。

因為Tick()也是在等待結束的時候傳送資料到通道,所以它的返回值是一個channel,從這個channel中可讀取每次等待完時的時間點。

下面是一個Tick()和After()結合的示例:

package main

import (
    "fmt"
    "time"
)

func main() {
    select {
    case <-time.Tick(2 * time.Second):
        fmt.Println("2 second over:", time.Now().Second())
    case <-time.After(7 * time.Second):
        fmt.Println("5 second over, timeover", time.Now().Second())
        return
    }
}

上面的示例,在等待2秒之後,就會因為讀取到了time.Tick()的通道資料而終止,因為select並未在迴圈內。

如果select在迴圈內,第二個case將永遠選擇不到。因為每次select輪詢中,第一個case都因為2秒而先被選中,使得第二個case的評估總是被中斷。進入下一個select輪詢後,又會重新開始評估兩個case,分別等待2秒和7秒。

func main() {
    for {
        select {
        case <-time.Tick(2 * time.Second):
            fmt.Println("2 second over:", time.Now().Second())
        case <-time.After(7 * time.Second):
            fmt.Println("5 second over, timeover", time.Now().Second())
            return
        }
    }
}

上面不正常執行的原因是因為每次select都會重新評估這些表示式。如果把這些表示式放在select外面,則正常:

package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(1 * time.Second)
    after := time.After(7 * time.Second)
    fmt.Println("start second:",time.Now().Second())
    for {
        select {
        case <-tick:
            fmt.Println("1 second over:", time.Now().Second())
        case <-after:
            fmt.Println("7 second over:", time.Now().Second())
            return
        }
    }
}

返回:

start second: 9
1 second over: 10
1 second over: 11
1 second over: 12
1 second over: 13
1 second over: 14
1 second over: 15
1 second over: 16
7 second over: 16

將time.Tick()和time.After()放在for...select的外面,使得select每次只評估通道是否可讀、可寫事件,而不會重新執行time.Tick()和time.After(),使得它們重新進入計時狀態。

注意上面的輸出結果中,有兩行:

1 second over: 16
7 second over: 16

說明在第16秒的時候,兩個case都評估為真了,但是這一次選擇了第一個case,然後進入下一個select過程,因為select的隨機選擇性,它會保證所有滿足條件的case儘量均衡分佈,這次將選擇第二個case,它仍然為第16秒,這時因為一次for和select呼叫所花的時間不可能會超過1秒而進入第17秒。