Go中定时器实现原理及源码解析 (6)

我们从一开始调用 runtime.addtimer 添加 timer 的时候,就会 runtime.wakeNetPoller来中断 netpoll ,那么它是如何做到的?我们下面先来看一个官方的例子:

func TestNetpollBreak(t *testing.T) { if runtime.GOMAXPROCS(0) == 1 { t.Skip("skipping: GOMAXPROCS=1") } // 初始化 netpoll runtime.NetpollGenericInit() start := time.Now() c := make(chan bool, 2) go func() { c <- true // netpoll 等待时间 runtime.Netpoll(10 * time.Second.Nanoseconds()) c <- true }() <-c loop: for { runtime.Usleep(100) // 中断netpoll 等待 runtime.NetpollBreak() runtime.NetpollBreak() select { case <-c: break loop default: } } if dur := time.Since(start); dur > 5*time.Second { t.Errorf("netpollBreak did not interrupt netpoll: slept for: %v", dur) } }

在上面这个例子中,首先会调用 runtime.Netpoll进行阻塞等待,然后循环调度 runtime.NetpollBreak进行中断阻塞。

runtime.netpoll

func netpoll(delay int64) gList { if epfd == -1 { return gList{} } var waitms int32 // 因为传入delay单位是纳秒,下面将纳秒转换成毫秒 if delay < 0 { waitms = -1 } else if delay == 0 { waitms = 0 } else if delay < 1e6 { waitms = 1 } else if delay < 1e15 { waitms = int32(delay / 1e6) } else { waitms = 1e9 } var events [128]epollevent retry: // 等待文件描述符转换成可读或者可写 n := epollwait(epfd, &events[0], int32(len(events)), waitms) // 返回负值,那么重新调用epollwait进行等待 if n < 0 { ... goto retry } var toRun gList for i := int32(0); i < n; i++ { ev := &events[i] if ev.events == 0 { continue } // 如果是 NetpollBreak 中断的,那么执行 continue 跳过 if *(**uintptr)(unsafe.Pointer(&ev.data)) == &netpollBreakRd { if ev.events != _EPOLLIN { println("runtime: netpoll: break fd ready for", ev.events) throw("runtime: netpoll: break fd ready for something unexpected") } if delay != 0 { var tmp [16]byte read(int32(netpollBreakRd), noescape(unsafe.Pointer(&tmp[0])), int32(len(tmp))) atomic.Store(&netpollWakeSig, 0) } continue } ... } return toRun }

在 调用runtime.findrunnable执行抢占时,最后会传入一个时间,超时阻塞调用 netpoll,如果没有事件中断,那么循环调度会一直等待直到 netpoll 超时后才往下进行:

func findrunnable() (gp *g, inheritTime bool) { ... delta := int64(-1) if pollUntil != 0 { // checkTimers ensures that polluntil > now. delta = pollUntil - now } ... // poll network // 休眠前再次检查 poll 网络 if netpollinited() && (atomic.Load(&netpollWaiters) > 0 || pollUntil != 0) && atomic.Xchg64(&sched.lastpoll, 0) != 0 { ... // 阻塞调用 list := netpoll(delta) } ... // 休眠当前 M stopm() goto top }

所以在调用 runtime.addtimer 添加 timer 的时候进行 netpoll 的中断操作可以更加灵敏的响应 timer 这类时间敏感的任务。

总结

我们通过 timer 的 1.13版本以及1.14版本后的对比可以发现,即使是一个定时器 go 语言都做了相当多的优化工作。从原来的需要维护 64 个桶,然后每个桶里面跑异步任务,到现在的将 timer列表直接挂到了 P 上面,这不仅减少了上下文切换带来的性能损耗,也减少了在锁之间的争抢问题,通过这些优化后有了可以媲美时间轮的性能表现。

Reference

go1.14基于netpoll优化timer定时器实现原理

https://github.com/golang/go/commit/6becb033341602f2df9d7c55cc23e64b925bbee2

https://github.com/golang/go/commit/76f4fd8a5251b4f63ea14a3c1e2fe2e78eb74f81

计时器 https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/timer/

《Golang》Netpoll解析 https://www.pefish.club/2020/05/04/Golang/1011Netpoll解析/

time.Timer 源码分析

luozhiyun很酷

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpddsg.html