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


Go channel系列

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM