Go 的 Windows 移植版在 Go 1.23 中增加了对高精度计时器的支持,将精度从约 15.6 毫秒提高到约 0.5 毫秒。这一增强影响了 time.Timer[1] 、 time.Ticker[2] 以及使 goroutine 休眠的函数,如 time.Sleep[3] 。
Gopher 图片由 Maria Letta[4] 创作,根据 CC0 1.0[5] 许可使用。
在 Windows 上使用高精度计时器一直是一个长期存在的需求,在 golang/go#44343[6] 中进行了跟踪,并由我们( Microsoft Go 团队[7] )在 CL 488675[8] 中实现。需要注意的是,Go 在 1.16 版本之前在 Windows 上使用了高精度计时器,但在 CL 232298[9] 中被移除,因为它们的实现不幸与 Go 调度器发生冲突,导致延迟问题。直到最近,这个问题还没有一个好的解决方案。
让我们来探讨一下高精度计时器是如何重新回到 Go 标准库中的,以及为什么它很重要。
朴素方法
使用 Win32 高精度计时器 API 的基本 time.Sleep
实现(我们从现在开始称之为朴素 Sleep
)可能如下所示:
func Sleep(d Duration) { timer := createWaitableTimer() setWaitableTimer(timer, d) waitForSingleObject(timer) closeHandle(timer) }
然而,这种方法会阻塞 OS 线程,这与 Go 的并发模型不太协调。Go 的 goroutine 被多路复用到少量线程上。直接阻塞线程会严重限制 Go 的并发能力。
考虑使用朴素 Sleep
实现的这段代码:
func main() { go func() { for i := 0; i < 10; i++ { fmt.Print(i) } }() Sleep(time.Second) fmt.Println("Done") }
你可能期望它打印 0123456789Done
,但实际上它只打印 Done
。为什么?因为单个线程被主 goroutine 中的朴素 Sleep
阻塞,阻止了计数 goroutine 的运行。
请注意,如果朴素 Sleep
使用 syscall
包实现,这段代码是可以正常工作的。Go 运行时总是将系统调用调度到一个单独的线程上,该线程不计入 GOMAXPROCS
限制。不幸的是,为每个计时器可能创建一个新线程是低效的,并可能降低计时器的有效分辨率,所以 Go 标准库中没有使用这个技巧。
Go 调度器解决方案
time[10] 包与 Go 调度器紧密集成,后者负责决定下一个应该执行哪个 goroutine,并将 goroutine 调度到内核线程上。当调用 time.Sleep
时,调度器会将一个虚拟计时器添加到活动计时器列表中。调度器检查这个列表,以在计时器到期时唤醒相关的 goroutine。如果没有待处理的工作且没有计时器到期,调度器会使当前线程休眠,直到下一个计时器的到期时间。
你可能认为朴素 Sleep
在这里可以工作,但在计时器到期之前可能会出现新的工作。Go 调度器需要唤醒休眠的线程来处理这个新工作,而这在朴素实现中是不可能的。
在 Windows 上,Go 调度器通过使用 I/O 完成端口[11] (IOCP) 解决了并发 I/O 操作和计时器的问题。它调用 GetQueuedCompletionStatusEx[12] 来等待 I/O 工作,最多等待到指定的超时时间,即下一个计时器的到期时间。简化后,Go 1.23 之前的 time.Sleep
实现如下:
func Sleep(d Duration) { timer := createTimer(d) for !timer.expired() { getQueuedCompletionStatusEx(iocp, timer.deadline()) } }
问题是这个函数的分辨率约为 15.6 毫秒,这对 time
包来说是不够的。
引入高分辨率计时器
从 Go 1.23 开始,Go 调度器使用 Windows API NtAssociateWaitCompletionPacket[13] 将高分辨率计时器与 IOCP 端口关联。当计时器到期时,Windows 内核会唤醒休眠的线程 - 如果它还没有被新到达的 I/O 工作唤醒的话。然后,Go 调度器使用该线程来运行休眠的 goroutine。
这个解决方案既优雅又简单,而且因为它只使用了自 Windows 10 以来可用的 API,所以它可以在 Go Windows 移植版支持的所有 Windows 版本上工作。如果能在 Go 1.16 中实现这个解决方案而不是放弃高分辨率计时器就好了。从技术上讲,这是可能的:Windows API 已经存在。然而,NtAssociateWaitCompletionPacket
直到 2023 年才被记录,依赖 未记录的 API[14] 不是一个好主意。
回到简化的代码,最终的实现看起来像这样:
func Sleep(d Duration) { timer := createTimer(d) for !timer.expired() { getQueuedCompletionStatusEx(iocp, timer.deadline()) } } func createTimer(d Duration) { timer := createWaitableTimer() setWaitableTimer(timer, d) ntAssociateWaitCompletionPacket(iocp, timer) return timer }
为什么这很重要?
高分辨率计时器对于需要精确计时的应用程序至关重要。
最近向 Go 问题跟踪器报告的一个例子是 golang/go#61665[15] ,其中默认的 Go CPU 分析器采样率 100 赫兹(每 10 毫秒一个样本)对于普通的 Windows 计时器分辨率来说太高了,导致分析数据不准确。
另一个向 Go 问题跟踪器报告的例子是 golang/go#44343[16] ,其中 golang.org/x/time/rate[17] 在 Windows 上当速率限制太高时限制过于激进,导致吞吐量低于预期。
还有一些情况下,高分辨率计时器对于正确的行为并不是必需的,但没有它们可能会导致意外的减速。Go 开发者经常调用 time.Sleep(time.Millisecond)
来等待一小段时间,仅在 GitHub 上就有 至少 24,200 次出现[18] !最糟糕的情况是涉及循环的情况,即使是一个无害的 for range 60 { time.Sleep(time.Millisecond) }
也可能需要 1 秒而不是预期的 0.06 秒来完成。
这项增强功能从 Go 1.23 开始可用,所以请确保更新你的 Go 安装以利用它。
请继续关注更多更新,祝编码愉快! 🚀
参考链接
- time.Timer: https://pkg.go.dev/time#Timer
- time.Ticker: https://pkg.go.dev/time#Ticker
- time.Sleep: https://pkg.go.dev/time#Sleep
- Maria Letta: https://creativemarket.com/Maria_Letta
- CC0 1.0: https://creativecommons.org/public-domain/cc0/
- golang/go#44343: https://github.com/golang/go/issues/44343
- Microsoft Go 团队: https://devblogs.microsoft.com/go/welcome-to-the-microsoft-for-go-developers-blog/
- CL 488675: https://go-review.googlesource.com/c/go/+/488675
- CL 232298: https://go-review.googlesource.com/c/go/+/232298
- time: https://pkg.go.dev/time
- I/O 完成端口: https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports
- GetQueuedCompletionStatusEx: https://learn.microsoft.com/en-us/windows/win32/fileio/getqueuedcompletionstatusex-func
- NtAssociateWaitCompletionPacket: https://learn.microsoft.com/en-us/windows/win32/devnotes/ntassociatewaitcompletionpacket
- 未记录的 API: https://learn.microsoft.com/en-us/windows/win32/w8cookbook/undocumented-apis
- golang/go#61665: https://github.com/golang/go/issues/61665
- golang/go#44343: https://github.com/golang/go/issues/44343#issuecomment-819032865
- golang.org/x/time/rate: https://pkg.dev.go/golang.org/x/time/rate
- 至少 24,200 次出现: https://github.com/search?q=language%3AGo+time.Sleep(time.Millisecond)&type=code&p=1