书接上回。昨天我写了一篇《这个限流库两个大bug存在了半年之久,没人发现?》,提到了Go语言中的time.Sleep函数的问题。有网友也私下和我探讨,提到这个可能属于系统的问题,因为现代的操作系统都是分时操作系统,每个线程可能会分配一个或者多个时间片,Windows默认线程时间精度在15毫秒,Linux在1毫秒,所以time.Sleep的精度不可能那么高。
本文原文有误导,结论部分正确,我在文章末尾作了更正
嗯,理论上这可以解释time.Sleep的行为,但是没有办法解释网友提出的在go 1.16之前的版本中,time.Sleep的精度更高,而go 1.16之后的版本中,time.Sleep的精度更低的问题。
这个问题在Go的bug系统中有很多,不只是单单上篇文章介绍的#44343, 比如#29485、#61456、#44476、#44608、#61042。这些bug中Ian Lance Taylor的有些评论很有价值,对于了解Go运行时的Sleep很有帮助。但是阅览了这么多的bug,没有人给出为啥go 1.16之后的版本中,time.Sleep的精度更低的解释,到底发生了啥?或许和Timer调度的变化有关。
Linux和Windows提供了更高精度的Sleep, Go开发者也在尝试解决Windows中过长的问题。
为了把这个问题说明白,我们举一个典型的例子,这里我使用了loov/hrtime ,它能提供更高精度的时间和benchmark方法。看到作者的名字我觉得眼熟,果然,作者的一个项目lensm也非常有名。
1 2 3 4 5 6 7 8 9 10 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轻量级服务器上测试的,可以看到time.Sleep休眠1毫秒以上还是和实际差不太多的,但是休眠1纳秒是不太可能的,这也符合我们的预期,只是实际休眠的时间是380纳秒还是挺长的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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提供了一个更高精度的系统调用nanosleep,可以提供纳秒级别的休眠,它是一个阻塞的系统调用,会阻塞当前线程,直到睡眠结束或被中断。
nanosleep系统调用和标准库的time.Sleep的主要区别:
阻塞方式不同:
nanosleep 会阻塞当前线程,直到睡眠结束或被中断
time.Sleep 会阻塞当前 goroutine
精度不同:
nanosleep 可以精确到纳秒
time.Sleep 最高只能精确到毫秒
中断处理不同:
nanosleep 可以通过信号中断并立即返回
time.Sleep 不可以中断,只能等待睡眠期满
用途不同:
nanosleep 主要用于需要精确睡眠时间的低级控制
time.Sleep 更适合高级逻辑控制,不需要精确睡眠时间
我们使用上面的测试代码,使用nanosleep替换time.Sleep,看看效果:
1 2 3 4 5 6 7 8 9 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 )) }
运行这段代码可以得到结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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可以提高精度,那么我们能不能以后就使用这个系统调用来代替time.Sleep呢?答案是视情况而定,你需要注意nanosleep是一个阻塞的系统调用,Go程序在调用它时,会将当前线程阻塞,直到休眠结束或者被中断,它会额外占用一个线程。如果你的程序中有很多的goroutine,那么你的程序可能会因为阻塞而导致性能下降。所以你需要权衡一下,如果你的程序中有很多的goroutine,而且你的程序中的goroutine需要休眠,那么你可以考虑使用time.Sleep,如果你的程序中的goroutine不多,而且你的程序中的goroutine需要精确的休眠时间,那么你可以考虑使用nanosleep。
而且,当前Go并不会将nanosleep占用的线程主动释放,而且放在池中备用,在并发nanosleep调用的时候,可能会导致线程数暴增,下面的代码演示了这个情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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()) }
在我的轻量级服务器上,显示结果如下:
1 2 threads in starting: 4 threads in nanosleep: 103
在nanosleep并发运行的时候,可以看到线程数达到了103个。线程数暴增会导致系统资源的浪费,而且程序性能也会下降。
当然如果你对threadcreate有疑义,也可以使用pstree查看程序当前的线程数。
线程不会释放的问题,已经在Go的bug系统中提出了,但是目前还没有解决,不过你可以通过增加runtime.LockOSThread()这个技巧来释放线程。注意没有调用UnlockOSThread():
1 2 3 4 5 6 for i := 0 ; i < 100 ; i++ { go func () { syscall.Nanosleep(&req, nil ) runtime.LockOSThread() }() }
本文并没有对生产环境做任何的建议,只是分析了:
time.Sleep和nanosleep的精度问题
nanosleep的使用方法
nanosleep的陷阱
算是对上一篇文章的延伸。
更正 基于网友在评论中指出我把时间单位看错了,我重新测试并做了更正。老眼昏花,先前确实看错了。
我想基于三个interval做测试: 50纳秒、50微秒、1毫秒。我使用了time.Sleep和syscall.Nanosleep两种方式,在阿里云的一台轻量级虚机(Ubuntu 22.04)上做了测试。
因为先前大家反映time.Sleep的精度不高,Linux只能达到毫秒级别,所以使用了50纳秒和50微秒。
下面是time.Sleep的测试结果: 50纳秒的情况下,实际达到了954纳秒,比预期的50纳秒要高很多。 50微秒的情况下,实际达到了1.08毫秒,比预期的50微秒要高很多。 1毫秒的情况下,实际达到了1.07毫秒,和预期的1毫秒差不多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 ```sh sleep 50ns avg 954ns; min 350ns; p50 421ns; max 30.3µs; p90 573ns; p99 30.3µs; p999 30.3µs; p9999 30.3µs; 350ns [ 98] ████████████████████████████████████████ 5µs [ 0] 10µs [ 0] 15µs [ 1] 20µs [ 0] 25µs [ 0] 30µs [ 1] 35µs [ 0] 40µs [ 0] 45µs [ 0] sleep 50µs avg 1.08ms; min 1.05ms; p50 1.08ms; max 1.14ms; p90 1.09ms; p99 1.14ms; p999 1.14ms; p9999 1.14ms; 1.05ms [ 1] ▌ 1.06ms [ 1] ▌ 1.07ms [ 24] █████████████████ 1.08ms [ 56] ████████████████████████████████████████ 1.09ms [ 9] ██████ 1.1ms [ 6] ████ 1.11ms [ 2] █ 1.12ms [ 0] 1.13ms [ 0] 1.14ms [ 1] ▌ sleep 1ms avg 1.07ms; min 1.06ms; p50 1.07ms; max 1.11ms; p90 1.09ms; p99 1.11ms; p999 1.11ms; p9999 1.11ms; 1.06ms [ 2] ██ 1.07ms [ 35] ████████████████████████████████████████ 1.07ms [ 6] ██████▌ 1.08ms [ 10] ███████████ 1.08ms [ 19] █████████████████████▌ 1.09ms [ 9] ██████████ 1.09ms [ 8] █████████ 1.1ms [ 8] █████████ 1.1ms [ 1] █ 1.11ms+[ 2] ██
下面是syscall.Nanosleep的测试结果。 在50纳秒的情况下,实际达到了62.9毫秒,比预期的50纳秒要高了很多。 在50微秒的情况下,实际达到了118微秒,比预期的50微秒要翻倍,但是比time.Sleep要好很多。 在1毫秒的情况下,实际达到了1.06毫秒,和预期的1毫秒差不多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 nanosleep 50ns avg 62.9µs; min 58.7µs; p50 60.6µs; max 130µs; p90 65.7µs; p99 130µs; p999 130µs; p9999 130µs; 58.7µs [ 32] █████████████████████▌ 60µs [ 59] ████████████████████████████████████████ 70µs [ 5] ███ 80µs [ 2] █ 90µs [ 1] ▌ 100µs [ 0] 110µs [ 0] 120µs [ 0] 130µs [ 1] ▌ 140µs [ 0] nanosleep 50µs avg 118µs; min 109µs; p50 111µs; max 223µs; p90 137µs; p99 223µs; p999 223µs; p9999 223µs; 109µs [ 83] ████████████████████████████████████████ 120µs [ 8] ███▌ 140µs [ 7] ███ 160µs [ 0] 180µs [ 0] 200µs [ 1] 220µs [ 1] 240µs [ 0] 260µs [ 0] 280µs [ 0] nanosleep 1ms avg 1.06ms; min 1.06ms; p50 1.06ms; max 1.08ms; p90 1.07ms; p99 1.08ms; p999 1.08ms; p9999 1.08ms; 1.06ms [ 10] █████▌ 1.07ms [ 69] ████████████████████████████████████████ 1.07ms [ 13] ███████▌ 1.08ms [ 3] █▌ 1.08ms [ 2] █ 1.09ms [ 3] █▌ 1.09ms [ 0] 1.1ms [ 0] 1.1ms [ 0] 1.11ms [ 0]
综合比较,syscall.Nanosleep的精度要比time.Sleep高很多,纳秒级别的精度虽然达不到,但是在微秒级别的精度上勉强还可以接受。