go語言更高精度的Sleep實例解析
引言
書接上回,寫了一篇《這個限流庫兩個大 bug 存在了半年之久,沒人發(fā)現(xiàn)?》,提到了 Go 語言中的time.Sleep
函數(shù)的問題。有網(wǎng)友也私下和我探討,提到這個可能屬于系統(tǒng)的問題,因為現(xiàn)代的操作系統(tǒng)都是分時操作系統(tǒng),每個線程可能會分配一個或者多個時間片,Windows 默認線程時間精度在 15 毫秒,Linux 在 1 毫秒,所以time.Sleep
的精度不可能那么高。
嗯,理論上這可以解釋time.Sleep
的行為,但是沒有辦法解釋網(wǎng)友提出的在go 1.16
之前的版本中,time.Sleep
的精度更高,而go 1.16
之后的版本中,time.Sleep
的精度更低的問題。
time.Sleep精度更低的問題
這個問題在 Go 的 bug 系統(tǒng)中有很多,不只是單單上篇文章介紹的#44343, 比如#29485、#61456、#44476、#44608、#61042。這些 bug 中 Ian Lance Taylor 的有些評論很有價值,對于了解 Go 運行時的 Sleep 很有幫助。但是閱覽了這么多的 bug,沒有人給出為啥go 1.16
之后的版本中,time.Sleep
的精度更低的解釋,到底發(fā)生了啥?或許和 Timer 調(diào)度的變化有關(guān)。
Linux 和 Windows 提供了更高精度的 Sleep, Go 開發(fā)者也在嘗試解決 Windows 中過長的問題。
為了把這個問題說明白,我們舉一個典型的例子,這里我使用了loov/hrtime[1],它能提供更高精度的時間和 benchmark 方法??吹阶髡叩拿治矣X得眼熟,果然,作者的一個項目 lensm 也非常有名。
intervals := []time.Duration{time.Nanosecond, time.Millisecond, 50 * time.Millisecond} for _, interval := range intervals { fmt.Printf("sleep %v\n", interval) b := hrtime.NewBenchmark(100) for b.Next() { time.Sleep(interval) } fmt.Println(b.Histogram(10)) }
休眠
我們嘗試使用time.Sleep
休眠 1 納秒、1 微秒和 50 微秒,可以看到實際休眠的時間基本在380ns
、1ms
、50ms
。我是在騰訊云上的一臺 Linux 輕量級服務(wù)器上測試的,可以看到time.Sleep
休眠 1 毫秒以上還是和實際差不太多的,但是休眠 1 納秒是不太可能的,這也符合我們的預(yù)期,只是實際休眠的時間是 380 納秒還是挺長的。
ubuntu@lab:~/workplace/timer$ go run main.go
sleep 1ns
avg 726ns; min 380ns; p50 476ns; max 22.4µs;
p90 670ns; p99 22.4µs; p999 22.4µs; p9999 22.4µs;
380ns [ 99] ████████████████████████████████████████
5µs [ 0]
10µs [ 0]
15µs [ 0]
20µs [ 1]
25µs [ 0]
30µs [ 0]
35µs [ 0]
40µs [ 0]
45µs [ 0]
sleep 1ms
avg 1.06ms; min 1.02ms; p50 1.06ms; max 1.09ms;
p90 1.07ms; p99 1.09ms; p999 1.09ms; p9999 1.09ms;
1.02ms [ 2] █▌
1.03ms [ 6] █████
1.04ms [ 0]
1.05ms [ 1] ▌
1.06ms [ 48] ████████████████████████████████████████
1.07ms [ 39] ████████████████████████████████
1.08ms [ 3] ██
1.09ms [ 1] ▌
1.1ms [ 0]
1.11ms [ 0]
sleep 50ms
avg 50.1ms; min 50.1ms; p50 50.1ms; max 50.1ms;
p90 50.1ms; p99 50.1ms; p999 50.1ms; p9999 50.1ms;
50.1ms [ 2] ██
50.1ms [ 0]
50.1ms [ 0]
50.1ms [ 1] █
50.1ms [ 13] ███████████████
50.1ms [ 34] ████████████████████████████████████████
50.1ms [ 31] ████████████████████████████████████
50.2ms [ 15] █████████████████▌
50.2ms [ 2] ██
50.2ms [ 2] ██
其實 Linux 提供了一個更高精度的系統(tǒng)調(diào)用nanosleep
,可以提供納秒級別的休眠,它是一個阻塞的系統(tǒng)調(diào)用,會阻塞當(dāng)前線程,直到睡眠結(jié)束或被中斷。
nanosleep系統(tǒng)調(diào)用和標準庫的time.Sleep的主要區(qū)別
阻塞方式不同:
nanosleep 會阻塞當(dāng)前線程,直到睡眠結(jié)束或被中斷
time.Sleep 會阻塞當(dāng)前 goroutine
精度不同:
nanosleep 可以精確到納秒
time.Sleep 最高只能精確到毫秒
中斷處理不同:
nanosleep 可以通過信號中斷并立即返回
time.Sleep 不可以中斷,只能等待睡眠期滿
用途不同:
nanosleep 主要用于需要精確睡眠時間的低級控制
time.Sleep 更適合高級邏輯控制,不需要精確睡眠時間
nanosleep替換time.Sleep
我們使用上面的測試代碼,使用nanosleep
替換time.Sleep
,看看效果:
for _, interval := range intervals { fmt.Printf("nanosleep %v\n", interval) req := syscall.NsecToTimespec(int64(interval)) b := hrtime.NewBenchmark(100) for b.Next() { syscall.Nanosleep(&req, nil) } fmt.Println(b.Histogram(10)) }
運行這段代碼可以得到結(jié)果:
nanosleep 1ns
avg 60.4µs; min 58.7µs; p50 60.2µs; max 77.5µs;
p90 61.2µs; p99 77.5µs; p999 77.5µs; p9999 77.5µs;
58.8µs [ 33] █████████████████████▌
60µs [ 61] ████████████████████████████████████████
62µs [ 1] ▌
64µs [ 3] █▌
66µs [ 0]
68µs [ 0]
70µs [ 1] ▌
72µs [ 0]
74µs [ 0]
76µs [ 1] ▌nanosleep 1ms
avg 1.06ms; min 1.03ms; p50 1.06ms; max 1.07ms;
p90 1.06ms; p99 1.07ms; p999 1.07ms; p9999 1.07ms;
1.04ms [ 1]
1.04ms [ 0]
1.05ms [ 0]
1.05ms [ 0]
1.06ms [ 0]
1.06ms [ 5] ██
1.07ms [ 92] ████████████████████████████████████████
1.07ms [ 1]
1.08ms [ 1]
1.08ms [ 0]nanosleep 50ms
avg 50ms; min 50ms; p50 50ms; max 50ms;
p90 50ms; p99 50ms; p999 50ms; p9999 50ms;
50.1ms [ 3] ███▌
50.1ms [ 5] ██████
50.1ms [ 26] █████████████████████████████████▌
50.1ms [ 31] ████████████████████████████████████████
50.1ms [ 18] ███████████████████████
50.1ms [ 16] ████████████████████▌
50.1ms [ 1] █
50.1ms [ 0]
50.1ms [ 0]
50.1ms [ 0]
可以看到在程序休眠 1 納秒時, nanosleep 實際休眠 60 納秒,相比于tome.Sleep
的 380 納秒,精度提高了很多。但是在休眠 1 毫秒和 50 毫秒時,nanosleep 和 time.Sleep 的精度差不多,都是 1 毫秒和 50 毫秒。
既然 nanosleep 可以提高精度,那么我們能不能以后就使用這個系統(tǒng)調(diào)用來代替time.Sleep
呢?答案是視情況而定,你需要注意nanosleep
是一個阻塞的系統(tǒng)調(diào)用,Go 程序在調(diào)用它時,會將當(dāng)前線程阻塞,直到休眠結(jié)束或者被中斷,它會額外占用一個線程。如果你的程序中有很多的 goroutine,那么你的程序可能會因為阻塞而導(dǎo)致性能下降。所以你需要權(quán)衡一下,如果你的程序中有很多的 goroutine,而且你的程序中的 goroutine 需要休眠,那么你可以考慮使用time.Sleep
,如果你的程序中的 goroutine 不多,而且你的程序中的 goroutine 需要精確的休眠時間,那么你可以考慮使用nanosleep
。
而且,當(dāng)前 Go 并不會將nanosleep
占用的線程主動釋放,而且放在池中備用,在并發(fā)nanosleep
調(diào)用的時候,可能會導(dǎo)致線程數(shù)暴增,下面的代碼演示了這個情況:
func Threads() { var threadProfile = pprof.Lookup("threadcreate") fmt.Printf(("threads in starting: %d\n"), threadProfile.Count()) var sleepTime time.Duration = time.Hour req := syscall.NsecToTimespec(int64(sleepTime)) for i := 0; i < 100; i++ { go func() { syscall.Nanosleep(&req, nil) }() } time.Sleep(10 * time.Second) fmt.Printf(("threads in nanosleep: %d\n"), threadProfile.Count()) }
在我的輕量級服務(wù)器上,顯示結(jié)果如下:
threads in starting: 4
threads in nanosleep: 103
在nanosleep
并發(fā)運行的時候,可以看到線程數(shù)達到了103
個。線程數(shù)暴增會導(dǎo)致系統(tǒng)資源的浪費,而且程序性能也會下降。
當(dāng)然如果你對threadcreate
有疑義,也可以使用pstree
查看程序當(dāng)前的線程數(shù)。
線程不會釋放的問題,已經(jīng)在 Go 的 bug 系統(tǒng)中提出了,但是目前還沒有解決,不過你可以通過增加runtime.LockOSThread()
這個技巧來釋放線程。注意沒有調(diào)用 UnlockOSThread():
for i := 0; i < 100; i++ { go func() { syscall.Nanosleep(&req, nil) runtime.LockOSThread() }() }
本文并沒有對生產(chǎn)環(huán)境做任何的建議,只是分析了:
time.Sleep
和nanosleep
的精度問題nanosleep
的使用方法nanosleep
的陷阱
算是對上一篇文章的延伸。
參考資料
[1]
loov/hrtime: https://github.com/loov/hrtime
以上就是go語言更高精度的Sleep的詳細內(nèi)容,更多關(guān)于go高精度Sleep的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang中為什么Response.Body需要被關(guān)閉詳解
這篇文章主要給大家介紹了關(guān)于golang中為什么Response.Body需要被關(guān)閉的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08解決panic: assignment to entry in nil
這篇文章主要介紹了解決panic: assignment to entry in nil map問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2008-01-01