Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go高精度Sleep

go语言更高精度的Sleep实例解析

作者:晁岳攀(鸟窝) 鸟窝聊技术

这篇文章主要为大家介绍了go语言更高精度的Sleep实例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

书接上回,写了一篇《这个限流库两个大 bug 存在了半年之久,没人发现?》,提到了 Go 语言中的time.Sleep函数的问题。有网友也私下和我探讨,提到这个可能属于系统的问题,因为现代的操作系统都是分时操作系统,每个线程可能会分配一个或者多个时间片,Windows 默认线程时间精度在 15 毫秒,Linux 在 1 毫秒,所以time.Sleep的精度不可能那么高。

嗯,理论上这可以解释time.Sleep的行为,但是没有办法解释网友提出的在go 1.16之前的版本中,time.Sleep的精度更高,而go 1.16之后的版本中,time.Sleep的精度更低的问题。

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[1],它能提供更高精度的时间和 benchmark 方法。看到作者的名字我觉得眼熟,果然,作者的一个项目 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 微秒,可以看到实际休眠的时间基本在380ns1ms50ms。我是在腾讯云上的一台 Linux 轻量级服务器上测试的,可以看到time.Sleep休眠 1 毫秒以上还是和实际差不太多的,但是休眠 1 纳秒是不太可能的,这也符合我们的预期,只是实际休眠的时间是 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 提供了一个更高精度的系统调用nanosleep,可以提供纳秒级别的休眠,它是一个阻塞的系统调用,会阻塞当前线程,直到睡眠结束或被中断。

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))
 }

运行这段代码可以得到结果:

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调用的时候,可能会导致线程数暴增,下面的代码演示了这个情况:

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())
}

在我的轻量级服务器上,显示结果如下:

threads in starting: 4
threads in nanosleep: 103

nanosleep并发运行的时候,可以看到线程数达到了103个。线程数暴增会导致系统资源的浪费,而且程序性能也会下降。

当然如果你对threadcreate有疑义,也可以使用pstree查看程序当前的线程数。

线程不会释放的问题,已经在 Go 的 bug 系统中提出了,但是目前还没有解决,不过你可以通过增加runtime.LockOSThread()这个技巧来释放线程。注意没有调用 UnlockOSThread():

 for i := 0; i &lt; 100; i++ {
  go func() {
   syscall.Nanosleep(&amp;req, nil)
   runtime.LockOSThread()
  }()
 }

本文并没有对生产环境做任何的建议,只是分析了:

算是对上一篇文章的延伸。

参考资料

[1]

loov/hrtime: https://github.com/loov/hrtime

以上就是go语言更高精度的Sleep的详细内容,更多关于go高精度Sleep的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文