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秒。