1 timer
timer 简单来说就是1 个定时器,代表多少秒后执行,当创建1个timer,1秒钟过后,我们就能从timer.C 获取那个时刻的时间,因为系统在那个时刻将当前时间写入到timer.C 了,这时候我们就可以做自己的想做的事了。
package main
import (
"fmt"
"time"
)
func main() {
timer:=time.NewTimer(1*time.Second)
defer timer.Stop()
msg:=<-timer.C
fmt.Println("1秒后打印",msg)
}
//结果:1秒后打印 2021-11-02 22:43:42.2260892 +0800 CST m=+1.016042901
timer 结构
type Timer struct {
C <-chan Time //存储时间的管道
r runtimeTimer //底层存储timer 的堆实现
}
- timer 代表单一的事件
- 当timer 过期后,当前时间将会被发送到C
- timer 只能被NewTimer 或者 AfterFunc两个函数创建
runtimeTimer结构
type runtimeTimer struct {
pp uintptr
when int64 //什么时候触发timer
period int64 //如果是周期性任务,执行周期性任务的时间间隔
f func(interface{}, uintptr) // NOTE: must not be closure//到时候执行的回调函数
arg interface{} //执行任务的参数
seq uintptr//回调函数的参数,该参数仅在 netpoll 的应用场景下使用。
nextwhen int64//如果是周期性任务,下次执行任务时间
status uint32//timer 的状态
}
p 上存储timer 的结构
下面只展示跟timer 有关的字段
//Go\src\runtime\runtime2.go +604
type p struct {
.....
//堆顶元素什么时候执行
timer0When uint64
//如果有timer 修改为更早执行时间了,将会将执行时间更新到当更早时间
timerModifiedEarliest uint64
//操作timer 的互斥锁
timersLock mutex
//该p 上的所有timer,必须加锁去操作这个字段,因为不同的p 操作这个字段会有竞争关系
timers []*timer
//p 堆上所有的timer
numTimers uint32
//被标记为删除的timer,要么是我们调用stop,要么是timer 自己触发后过期导致的删除
deletedTimers uint32
....
}
为什么是四叉堆
但是与我们常见的,使用二叉树来实现最小堆不同,Golang 这里采用了四叉堆 (4-heap) 来实现。这里 Golang 并没有直接给出解释。 这里直接贴一段 知乎网友对二叉堆和 N 叉堆的分析。
- 上推节点的操作更快。假如最下层某个节点的值被修改为最小,同样上推到堆顶的操作,N 叉堆需要的比较次数只有二叉堆的
倍。
-
- 对缓存更友好。二叉堆对数组的访问范围更大,更加随机,而 N 叉堆则更集中于数组的前部,这就对缓存更加友好,有利于提高性能。
C 语言知名开源网络库 libev,其timer定时器实现可以在编译时选择采用四叉堆。在它的注释里提到四叉树相比来说缓存更加友好。 根据benchmark,在 50000 + 个 timer 的场景下,四叉树会有 5% 的性能优势。具体可见 libev/ev.c#L2227
NewTimer
//创建一个将会在Duration 时间后将那一刻的时间发生到C 的timer
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1) //创建1个channel
t := &Timer{ //创建一个timer
C: c,
r: runtimeTimer{
when: when(d), //什么时候执行
f: sendTime, //到时候执行的回调函数
arg: c,//执行参数
},
}
startTimer(&t.r) //开始timer
return t
}
- C 是一个带1个容量的chan,这样做有什么好处呢,原因是chan 无缓冲发送数据就会阻塞,阻塞系统协程,这显然是不行的。
- 回调函数设置为sendTime,执行参数为channel,sendTime就是到点往C 里面发送当前时间的函数
sendTime实现
//c interface{} 就是NewTimer 赋值的参数,就是channel
func sendTime(c interface{}, seq uintptr) {
select {
case c.(chan Time) <- Now(): //写不进去的话,C 已满,走default 分支
default:
}
}
- sendTime 是不阻塞的,在Timer 实现里面是不会被阻塞的,因为只写一次数据。但是在Ticker里面就会存在阻塞,因为容量为1,ticker 会按时间间隔周期性的写数据到C,这时候如果没有写进去,这次写事件就会丢弃。那么是怎么做到呢?
case c.(chan Time) <- Now() 的时候,如果C 里面的数据没人取走,那么C 已满,case 这条分支发送数据到C就会执行失败而走下面的default。相当于本次调用没有任何操作。 - 官方注释说:如果reader读C数据慢于第二次向C写数据,那么丢掉这次数据是理想的行为。
After
和NewTimer 一样 ,只是NewTimer 的语法糖,将创建的timer 的C返回了,这样就可以直接使用了
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
- 和 NewTimer(d).C 是等价的,等待Duration 后将触发,将当前时间写入到返回的 channel。
- 在计时器触发前,GC 是不会回收timer 的(内存泄漏的提醒,遇到问题时不要怪go 坑,官方可有说明~///(^v^)\\~),不再需要timer时,调用NewTimer 创建timer,然后去调用Timer.Stop效率更高。
AfterFunc
这个函数代表在多少秒后执行传入的回调函数f,并返回timer
func AfterFunc(d Duration, f func()) *Timer {
t := &Timer{
r: runtimeTimer{
when: when(d),//什么时候触发
f: goFunc, //到时候执行的回调函数
arg: f//参数为回调函数
},
}
startTimer(&t.r)
return t
}
//执行的回调函数
func goFunc(arg interface{}, seq uintptr) {
go arg.(func())()
}
- AfterFunc 等待Duration 后触发,在自己goroutine 触发执行回调函数f,返回可以通过调用stop 去放弃执行的timer。
- 最终执行的函数是goFunc,参数为用户传入的回调函数,当时间一到,启动goroutine 去执行任务。所以用这个执行回调函数修改共享数据很可能产生并发问题。
stop
func (t *Timer) Stop() bool {
if t.r.f == nil {
panic("time: Stop called on uninitialized Timer")
}
return stopTimer(&t.r) //调用系统的 stopTimer
}
- 从激活中阻止timer
- 当timer stop 时返回true,如果timer 已经过期(就是已经被激活)或者已经停止那么则返回false
package main
import (
"fmt"
"time"
)
//timer 正常stop
func Test1() {
timer:=time.NewTimer(1*time.Second)
fmt.Println(timer.Stop()) //true
}
//timer 已调用stop
func Test2() {
timer:=time.NewTimer(1*time.Second)
timer.Stop()
fmt.Println(timer.Stop()) //false
}
//timer 已被激活
func Test3() {
timer:=time.NewTimer(1*time.Second)
<-timer.C
fmt.Println(timer.Stop())//false
}
func main() {
Test1()
Test2()
Test3()
}
- stop 不会关闭channel,去阻止从channel 成功的读取是完全不正确的
- 确认channel 是空的在调用stop 后,检查返回值然后排空channel.
例如:假设t.c 里面的值还没被接收
if !t.Stop() {
<-t.C
}
- 注意事项:不能并发的从channel 里面读数据,也不能并发的调用stop ,我们来看看会发生什么事。因为并发的读chan,当chan 读完1 个数据以后,没有发送者了就会一直阻塞,造成死锁
package main
import (
"time"
)
func main() {
timer:=time.NewTimer(1*time.Second)
for i:=0;i<10;i++{
<-timer.C
}
}
//结果
//fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
D:/code/leetcode/timer.go:10 +0x59
来看看并发的调用stop
package main
import (
"fmt"
"time"
)
func main() {
timer:=time.NewTimer(1*time.Second)
for i:=0;i<100;i++{
go fmt.Println(timer.Stop())
}
}
//虽然没有错误产生,但是也是不可取的,多次执行返回的状态却不一定是正确的.
false
false
false
true //顺序不固定,多次执行结果不一样
false
- AfterFunc(d, f) 创建的timer 调用t.stop 返回false ,说明timer 过期了,f 在自己的goroutine 开始执行了,并且stop 不会等待f 执行完才返回,如果想知道f 是否已经完成,则需要自己明确的协商定义,自己去获取状态,例如下面的操作。根据结果可以看到,当timer 已经激活,timer.Stop返回false,但是f 并发没有执行完,我们如果想知道f 的状态,则需要自己操作获取状态。
package main
import (
"fmt"
"time"
)
func main() {
var isComplete bool
timer:=time.AfterFunc(1*time.Second, func() {
time.Sleep(5*time.Second)
fmt.Println("任务完成")
isComplete=true //此例子不是并发操作
})
time.Sleep(2*time.Second) //让timer 过期激活
fmt.Println(timer.Stop(),"isComplete=",isComplete) //true isComplete= false
for {
if isComplete{
break
}
}
fmt.Println("任务完成",isComplete)
}
//下面是执行结果
//false isComplete= false
//任务完成
//任务完成 true
2 ticker
Ticker 形容时钟滴答滴答的声音,在go 中常用来做定时任务,任务到了执行任务。
Ticker 使用案例,常用来做定时任务或者顶层连接心跳,每秒定时做什么
package main
import (
"fmt"
"time"
)
func main() {
t:=time.NewTicker(1*time.Second)
defer t.Stop()
for now:=range t.C{
fmt.Println(now)
}
}
Ticker 结构
type Ticker struct {
C <-chan Time //chan 定时到了以后,go 系统会忘里面添加一个当前时间的数据
r runtimeTimer
}
创建一个Ticker
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic(errors.New("non-positive interval for NewTicker"))
}
//这里预留一个缓冲给timer 一样,但是满了以后没人接收后面会丢掉事件
c := make(chan Time, 1)
t := &Ticker{
C: c,
r: runtimeTimer{
when: when(d),
period: int64(d),
f: sendTime, //和ticker 的函数一样
arg: c,
},
}
startTimer(&t.r)
return t
}
- 和timer 创建方式一样,只不过period为Duration,这样底层在检查时会根据这个字段判断是不是周期性timer,从而删掉原来的timer,创建新的timer
stop
调用stopTimer停止ticker,停止不会关闭 channel。channel也不能被并发读
func (t *Ticker) Stop() {
stopTimer(&t.r)
}
Reset
调用modTimer修改时间,接下来的激活将在新period后
func (t *Ticker) Reset(d Duration) {
if t.r.f == nil {
panic("time: Reset called on uninitialized Ticker")
}
modTimer(&t.r, when(d), int64(d), t.r.f, t.r.arg, t.r.seq)
}
Tick
- 返回ticker 的channel,和timer 一样,对于不需要关闭timer 的客户端有用,但是注意这里没有一个方式去关闭底层timer,所以也不会被垃圾回收
func Tick(d Duration) <-chan Time {
if d <= 0 {
return nil
}
return NewTicker(d).C
}
3 sleep
func timeSleep(ns int64) {
if ns <= 0 {
return
}
gp := getg()
t := gp.timer
if t == nil {
t = new(timer)
gp.timer = t
}
t.f = goroutineReady
t.arg = gp
t.nextwhen = nanotime() + ns
if t.nextwhen < 0 { // check for overflow.
t.nextwhen = maxWhen
}
gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceEvGoSleep, 1)
}
func resetForSleep(gp *g, ut unsafe.Pointer) bool {
t := (*timer)(ut)
resettimer(t, t.nextwhen)
return true
}
- sleep底层也是创建timer,执行函数为goroutineReady,参数为当前g,执行时间为当前时间+传进来的时间,然后调用gopark。停止协程,等待timer 过期被唤醒。
- resetForSleep 在goroutine停止后调用,我们不能自己调用resettimer,因为如果这是短暂的sleep,并且有大量goroutines,p 可能在goroutine被停止前提交调用goroutineReady
4 底层源码解析
注意:本文为go 在1.17.2 下面的源码解析,如果是1.15 或者1.16 可能有些不同
来看看下面这些函数,go/src/time 是找不到任何实现的,其实go 官方包🈶一个共性,找不到的都在rumtime里面,我们去runtime 里面找找。
func startTimer(*runtimeTimer)
func stopTimer(*runtimeTimer) bool
func resetTimer(*runtimeTimer, int64) bool
func modTimer(t *runtimeTimer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr)
在go/src/runtime/time.go,我们发现了这些函数的实现,下面我们来看看源码。
// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
if raceenabled {
racerelease(unsafe.Pointer(t))
}
addtimer(t)
}
// stopTimer stops a timer.
// It reports whether t was stopped before being run.
//go:linkname stopTimer time.stopTimer
func stopTimer(t *timer) bool {
return deltimer(t)
}
// resetTimer resets an inactive timer, adding it to the heap.
//go:linkname resetTimer time.resetTimer
// Reports whether the timer was modified before it was run.
func resetTimer(t *timer, when int64) bool {
if raceenabled {
racerelease(unsafe.Pointer(t))
}
return resettimer(t, when)
}
// modTimer modifies an existing timer.
//go:linkname modTimer time.modTimer
func modTimer(t *timer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr) {
modtimer(t, when, period, f, arg, seq)
}
- 下面这些是函数的真正实现,通过go:linkname链接到time里面的函数,所以可以官方源码看到很多时候go 函数并没有实现,但是能调用
timer 的状态字段
const (
// 默认状态,没有状态
timerNoStatus = iota
// 等待timer 去触发,在P 的堆上
timerWaiting
// 执行 timer function.timer 只会短暂的拥有这个状态
timerRunning
// timer 被删了,应该被从堆里面移除。
// 在这个状态下不要执行,但是任然在P 的堆上。
timerDeleted
// timer 将被移除
// 这个状态也是短暂的
timerRemoving
// timer 已经被移除了,并且不在P 的堆上面
timerRemoved
// The timer 将被修改,这个状态也是短暂的
timerModifying
// timer 被修改成更早的时间了,新的执行时间when 在nextWhen 上面
// timer 在P 的堆上, 可能在错误的地方.
timerModifiedEarlier
// timer 修改成更后面的时间了,新的执行时间when 在nextWhen 上面
// timer 在P 的堆上, 可能在错误的地方.
timerModifiedLater
// timer 被修改并且将被移除。该状态是短暂的
timerMoving
)
官方给的操作时的状态转换
//
// addtimer:
// timerNoStatus -> timerWaiting
// anything else -> panic: invalid value
// deltimer:
// timerWaiting -> timerModifying -> timerDeleted
// timerModifiedEarlier -> timerModifying -> timerDeleted
// timerModifiedLater -> timerModifying -> timerDeleted
// timerNoStatus -> do nothing
// timerDeleted -> do nothing
// timerRemoving -> do nothing
// timerRemoved -> do nothing
// timerRunning -> wait until status changes
// timerMoving -> wait until status changes
// timerModifying -> wait until status changes
// modtimer:
// timerWaiting -> timerModifying -> timerModifiedXX
// timerModifiedXX -> timerModifying -> timerModifiedYY
// timerNoStatus -> timerModifying -> timerWaiting
// timerRemoved -> timerModifying -> timerWaiting
// timerDeleted -> timerModifying -> timerModifiedXX
// timerRunning -> wait until status changes
// timerMoving -> wait until status changes
// timerRemoving -> wait until status changes
// timerModifying -> wait until status changes
// cleantimers (looks in P's timer heap):
// timerDeleted -> timerRemoving -> timerRemoved
// timerModifiedXX -> timerMoving -> timerWaiting
// adjusttimers (looks in P's timer heap):
// timerDeleted -> timerRemoving -> timerRemoved
// timerModifiedXX -> timerMoving -> timerWaiting
// runtimer (looks in P's timer heap):
// timerNoStatus -> panic: uninitialized timer
// timerWaiting -> timerWaiting or
// timerWaiting -> timerRunning -> timerNoStatus or
// timerWaiting -> timerRunning -> timerWaiting
// timerModifying -> wait until status changes
// timerModifiedXX -> timerMoving -> timerWaiting
// timerDeleted -> timerRemoving -> timerRemoved
// timerRunning -> panic: concurrent runtimer calls
// timerRemoved -> panic: inconsistent timer heap
// timerRemoving -> panic: inconsistent timer heap
// timerMoving -> panic: inconsistent timer heap
timer 的数据结构与算法
堆排序算法
- p 的timers是个数组,经过堆排序后维护,所用的堆是4叉小顶堆,每个p 都有一个这样的堆。
- 每次堆顶的timer 的when 都是最小的代表最先执行timer,所以我们只要不断循环获取堆顶这个timer 执行就行了。
siftupTimer
- 堆维护算法,手动的检查切片索引是否合法,切片索引错误通常发生在不优雅的访问timers上面,但是我们不能panic,因为这会引起严重的问题 ”在锁中panic “,相应的,我们panic 在没有上锁的时候。
- 堆上滤算法,将i 位置的timer 上滤,插入到合适的位置,就是和父节点比大小,如果小,则和父节点交换,直到找到合适的位置(比父节点大),注意这是四叉堆,所以父节点是(i - 1) / 4
func siftupTimer(t []*timer, i int) int {
if i >= len(t) {
badTimer()
}
when := t[i].when
if when <= 0 {
badTimer()
}
tmp := t[i]
for i > 0 {
p := (i - 1) / 4 // parent
if when >= t[p].when {
break
}
t[i] = t[p]
i = p
}
if tmp != t[i] {
t[i] = tmp
}
return i
}
siftdownTimer
- 堆算法下滤将上面的timer 不断往下找,直到找到合适的位置。
- 不断比较和child 的执行时间,找到4个child 执行时间更小的不断交换,直到找到合适的位置。
- 子child 分别是i*4 + 1,i*4 + 2,i*4 + 3,i*4 + 4
func siftdownTimer(t []*timer, i int) {
n := len(t)
if i >= n {
badTimer()
}
when := t[i].when
if when <= 0 {
badTimer()
}
tmp := t[i]
for {
c := i*4 + 1 // left child
c3 := c + 2 // mid child //第三个孩子
if c >= n {
break
}
w := t[c].when //先让w 获取第一个孩子的执行时间
if c+1 < n && t[c+1].when < w { //如果第二个孩子执行时间更小将w 换成第二个孩子
w = t[c+1].when
c++
}
if c3 < n {
w3 := t[c3].when
if c3+1 < n && t[c3+1].when < w3 { //再来比较低三个孩子和第四个孩子
w3 = t[c3+1].when
c3++
}
if w3 < w {//最后选出执行时间最小的孩子
w = w3
c = c3
}
}
if w >= when {
break
}
t[i] = t[c]
i = c
}
if tmp != t[i] {
t[i] = tmp
}
}
acquirem
在之前我们先聊聊这个函数,禁止抢占
//go:nosplit
func acquirem() *m {
_g_ := getg()
_g_.m.locks++
return _g_.m
}
抢占时机判断
在判断m 是否可以被抢占的时候,有下面几个条件
//C:\Go\src\runtime\preempt.go +287
func canPreemptM(mp *m) bool {
return mp.locks == 0 && mp.mallocing == 0 && mp.preemptoff == "" && mp.p.ptr().status == _Prunning
}
- 运行时没有禁止抢占(
m.locks == 0
) - 运行时没有在执行内存分配(
m.mallocing == 0
) - 运行时没有关闭抢占机制(
m.preemptoff == ""
) - M 与 P 绑定且没有进入系统调用(
p.status == _Prunning
)
所以acquirem将m.locks++,禁止抢占,调用mp.locks--,恢复可以抢占
//go:nosplit
func releasem(mp *m) {
_g_ := getg()
mp.locks--
if mp.locks == 0 && _g_.preempt {
// restore the preemption request in case we've cleared it in newstack
_g_.stackguard0 = stackPreempt
}
}
addtimer
使用:
- 添加一个timer,只能用在新建一个timer的时候,避免在p 的堆上改变timer的when 字段 而造成堆是无序的
func addtimer(t *timer) {
// when must be positive. A negative value will cause runtimer to
// overflow during its delta calculation and never expire other runtime
// timers. Zero will cause checkTimers to fail to notice the timer.
if t.when <= 0 {
throw("timer when must be positive")
}
if t.period < 0 {
throw("timer period must be non-negative")
}
if t.status != timerNoStatus {
throw("addtimer called with initialized timer")
}
t.status = timerWaiting
when := t.when
//禁用p被抢占去避免去改变其他p 的堆栈
// Disable preemption while using pp to avoid changing another P's heap.
mp := acquirem()
pp := getg().m.p.ptr() //获取当前p
lock(&pp.timersLock)
cleantimers(pp) //清除timers
doaddtimer(pp, t)//添加timer
unlock(&pp.timersLock)
wakeNetPoller(when)//添加到netpoller
releasem(mp)
}
- 就是将timer 加到当前执行p的timers数组里面去
- 整个过程中需要设置不可抢占,为什么需要设置为M 不可抢占了,因为如果被抢占了,此时操作的p 有可能发生变化,造成操作别人的堆。
- 调用
wakeNetPoller
方法:唤醒网络轮询器中休眠的线程,检查计时器被唤醒的时间(when)是否在当前轮询预期运行的时间(pollerPollUntil)内,若是唤醒。
doaddtimer
将timer 添加到当前p 的堆上,该函数在锁中执行
func doaddtimer(pp *p, t *timer) {
// Timers 通过netpoll 恢复执行
if netpollInited == 0 { //netpoll 没有初始化,重新初始化netpoll
netpollGenericInit()
}
if t.pp != 0 {
throw("doaddtimer: P already set in timer")
}
t.pp.set(pp) //设置timer 的pp
i := len(pp.timers)
pp.timers = append(pp.timers, t) //添加到timers,也是就是添加到堆尾
siftupTimer(pp.timers, i)//上滤
if t == pp.timers[0] {//如果是堆顶的话,还需要重新更新pp.timer0When
atomic.Store64(&pp.timer0When, uint64(t.when))
}
atomic.Xadd(&pp.numTimers, 1) //将总timer 数量+1
}
deltimer
使用:可见在netpoll 或者stopTimer 会使用该函数去将timer 状态标记为被删除timer
func deltimer(t *timer) bool {
for {
switch s := atomic.Load(&t.status); s {
case timerWaiting, timerModifiedLater:
// Prevent preemption while the timer is in timerModifying.
// This could lead to a self-deadlock. See #38070.
mp := acquirem()
if atomic.Cas(&t.status, s, timerModifying) {
//必须在改变状态前获取到pp,因为其他goroutine可能 清理掉将timerDeleted timer
tpp := t.pp.ptr()
if !atomic.Cas(&t.status, timerModifying, timerDeleted) {
badTimer()
}
releasem(mp)
atomic.Xadd(&tpp.deletedTimers, 1)
// Timer was not yet run.
return true
} else {
releasem(mp)
}
case timerModifiedEarlier:
// Prevent preemption while the timer is in timerModifying.
// This could lead to a self-deadlock. See #38070.
mp := acquirem()
if atomic.Cas(&t.status, s, timerModifying) {
// Must fetch t.pp before setting status
// to timerDeleted.
tpp := t.pp.ptr()
if !atomic.Cas(&t.status, timerModifying, timerDeleted) {
badTimer()
}
releasem(mp)
atomic.Xadd(&tpp.deletedTimers, 1)
// Timer was not yet run.
return true
} else {
releasem(mp)
}
case timerDeleted, timerRemoving, timerRemoved:
// Timer was already run.
return false
case timerRunning, timerMoving:
// The timer is being run or moved, by a different P.
// Wait for it to complete.
osyield()
case timerNoStatus:
// Removing timer that was never added or
// has already been run. Also see issue 21874.
return false
case timerModifying:
// Simultaneous calls to deltimer and modtimer.
// Wait for the other call to complete.
osyield()
default:
badTimer()
}
}
}
- 简单来说就是修改timer 的状态,先修改为将被修改状态,再修改为删除状态
- 因为都是操作timer.p,而且修改p.timers这个操作是可能有多个goroutine操作,所以我们要先获取timer.p 然后将被删除的timer 数量+1。如果没有在操作之前获取到timer.p,后面被标记为删除被其他goroutine清理掉了,那么我们就再也获取不到timer.p了。因为timer.p被置为0了。
cleantimers
- 加快了创建和删除计时器的程序的速度,将它们留在堆中会减慢添加时间,也报告有问题的timer,必须在pp上面加锁
func cleantimers(pp *p) {
gp := getg() //获取执行的gp
for {
if len(pp.timers) == 0 { //如果没有timers 返回
return
}
//从理论上来讲这个循环将会运行一段时间,因为加了timersLock不能被抢占,如果某人想抢占这个goroutine,我们返回等待下一次清理
if gp.preemptStop {
return
}
t := pp.timers[0] //获取堆顶元素
if t.pp.ptr() != pp {
throw("cleantimers: bad p")
}
switch s := atomic.Load(&t.status); s {
case timerDeleted: //被删除了,先将状态改为待移除
if !atomic.Cas(&t.status, s, timerRemoving) {
continue
}
//清除堆顶元素
dodeltimer0(pp)
//将状态改为移除
if !atomic.Cas(&t.status, timerRemoving, timerRemoved) {
badTimer()
}
//将被删除的timers 数量减一
atomic.Xadd(&pp.deletedTimers, -1)
case timerModifiedEarlier, timerModifiedLater:
//时间修改为更早或者更晚,将状态改为移除
if !atomic.Cas(&t.status, s, timerMoving) {
continue
}
// 将执行时间改为下一次移除
t.when = t.nextwhen
dodeltimer0(pp) //先删除堆顶
doaddtimer(pp, t)//重新加入timer
if !atomic.Cas(&t.status, timerMoving, timerWaiting) { //将状态置为等待触发
badTimer()
}
default:
// Head of timers does not need adjustment.
return
}
}
}
dodeltimer
真正执行删除
func dodeltimer(pp *p, i int) int {
if t := pp.timers[i]; t.pp.ptr() != pp {
throw("dodeltimer: wrong P")
} else {
t.pp = 0
}//判断timer 的状态
last := len(pp.timers) - 1
if i != last {
pp.timers[i] = pp.timers[last]
}//将timer 和对最后一个timer 交换
pp.timers[last] = nil
pp.timers = pp.timers[:last]
smallestChanged := i
if i != last { //i 等于last ,代表移除最后一个timer.不需要下滤或者上滤
//先上滤,然后下滤,这样保证有序了
smallestChanged = siftupTimer(pp.timers, i)
siftdownTimer(pp.timers, i)
}
if i == 0 { //更改堆顶元素需要,更新pp 上面堆顶元素状态
updateTimer0When(pp)
}
atomic.Xadd(&pp.numTimers, -1) //总数量减一
return smallestChanged
}
dodeltimer0
删除堆顶的timer
func dodeltimer0(pp *p) {
if t := pp.timers[0]; t.pp.ptr() != pp {
throw("dodeltimer0: wrong P")
} else {
t.pp = 0
}
last := len(pp.timers) - 1
if last > 0 {
pp.timers[0] = pp.timers[last]
}
pp.timers[last] = nil
pp.timers = pp.timers[:last]
if last > 0 {
siftdownTimer(pp.timers, 0)
}
updateTimer0When(pp)
atomic.Xadd(&pp.numTimers, -1)
}
- 取出堆顶的timer ,然后判端timer 所属pp 和当前执行的pp 是不是一样的,如果一样,则将timer的pp 置为0
因为堆顶timer 删除,但是timer 还是有引用的,将pp 置为0,代表该timer 不属于任何p了。 - 执行的堆顶删除流程
1、将堆顶和堆最后一个元素交换,将最后一个元素置为nil,将数组重新分配删除最后一个,下面这两句话可以保证被删除的元素垃圾回收,如果没有这句pp.timers[last] = nil,可能会内存泄露,因为最后一个元素有引用不会被回收。
pp.timers[last] = nil pp.timers = pp.timers[:last]
2、执行下滤操作
updateTimer0When
if len(pp.timers) == 0 {
atomic.Store64(&pp.timer0When, 0)
} else {
atomic.Store64(&pp.timer0When, uint64(pp.timers[0].when))
}
该字段将会更新pp 上面堆顶元素的执行时间,因为原来的被删除了,此时应该用一个新的去执行
modtimer
- 修改一个存在的timer
- 被调用在time.Ticker.Reset or time.Timer.Reset或者netpoll 上面
func modtimer(t *timer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr) bool {
if when <= 0 { //检验时间
throw("timer when must be positive")
}
if period < 0 {
throw("timer period must be non-negative")
}
status := uint32(timerNoStatus)
wasRemoved := false
var pending bool
var mp *m
loop:
for {
switch status = atomic.Load(&t.status); status {
case timerWaiting, timerModifiedEarlier, timerModifiedLater:
// 禁止p 被抢占
// This could lead to a self-deadlock. See #38070.
mp = acquirem()
//将状态置为待修改
if atomic.Cas(&t.status, status, timerModifying) {
pending = true // timer not yet run
break loop
}
releasem(mp)
case timerNoStatus, timerRemoved:
// 禁止p 被抢占
// This could lead to a self-deadlock. See #38070.
mp = acquirem()
// 禁止p 被抢占
// Act like addtimer.
if atomic.Cas(&t.status, status, timerModifying) {
wasRemoved = true
pending = false // timer already run or stopped
break loop
}
releasem(mp)
case timerDeleted:
// 禁止p 被抢占
// This could lead to a self-deadlock. See #38070.
mp = acquirem()
if atomic.Cas(&t.status, status, timerModifying) {
atomic.Xadd(&t.pp.ptr().deletedTimers, -1)
pending = false // timer 已经运行或者停止了
break loop
}
releasem(mp)
case timerRunning, timerRemoving, timerMoving:
// timer 被当前p 运行或者移除
// 等待状态完成
osyield()
case timerModifying:
// modtimer被多个同时调用
// 等待状态完成
osyield()
default:
badTimer()
}
}
t.period = period
t.f = f
t.arg = arg
t.seq = seq
if wasRemoved { //如果timer 被移除了
t.when = when
pp := getg().m.p.ptr()
lock(&pp.timersLock)
doaddtimer(pp, t) //添加这个timer,注意要加锁,因为不知道哪个p 在操作,会有数据竞争
unlock(&pp.timersLock)
if !atomic.Cas(&t.status, timerModifying, timerWaiting) { //将timer 修改为等待
badTimer()
}
releasem(mp)
wakeNetPoller(when)//加入netpoller 等待唤醒
} else {//如果没有被移除,因为timer 在某些p 的堆上,所以我们不能改变这个字段,如果我们改变了,其他p 的堆将会无序了,所以我们赋值给nextwhen ,让其他p 自己去操作when 字段去重排序堆
t.nextwhen = when
newStatus := uint32(timerModifiedLater) //更新状态,根据修改时间判断时修改为更早了还是更晚了
if when < t.when {
newStatus = timerModifiedEarlier
}
tpp := t.pp.ptr()
if newStatus == timerModifiedEarlier { //修改为更早了,就更新为更早
updateTimerModifiedEarliest(tpp, when)
}
//更新timerModifying状态
if !atomic.Cas(&t.status, timerModifying, newStatus) {
badTimer()
}
releasem(mp)
// If the new status is earlier, wake up the poller.
if newStatus == timerModifiedEarlier {
wakeNetPoller(when) //加入netpoller 等待唤醒
}
}
return pending
}
osyield
简单来说就相当于sleep,精度更小,暂停一些,等待其他协程执行
该函数在不同平台的实现不一样
linux(src\runtime\os_linux.go +411))
空实现,简单来说就是不做任何事情
func osyield()
mac(src\runtime\os_darwin.go +347)
//go:nosplit
func osyield() {
usleep(1)
}
//go:nosplit
//高精度的timer 可以用,优先使用高优先级的,不能话就只能使用低优先级的
func usleep(us uint32) {
systemstack(func() {
dt := -10 * int32(us) // relative sleep (negative), 100ns units
// If the high-res timer is available and its handle has been allocated for this m, use it.
// Otherwise fall back to the low-res one, which doesn't need a handle.
if haveHighResTimer && getg().m.highResTimer != 0 {
usleep2HighRes(dt)
} else {
usleep2(dt)
}
})
}
windows(src\runtime\os_darwin.go +1157)
//go:nosplit
func osyield() {
systemstack(switchtothread)
}
// systemstack runs fn on a system stack.
//在系统栈上(g0)调用fn或者在gsignal 栈
//调用并且直接返回
// Otherwise, systemstack is being called from the limited stack
// of an ordinary goroutine. In this case, systemstack switches
// to the per-OS-thread stack, calls fn, and switches back.
// It is common to use a func literal as the argument, in order
// to share inputs and outputs with the code around the call
// to system stack:
//
// ... set up y ...
// systemstack(func() {
// x = bigcall(y)
// })
// ... use x ...
//
//go:noescape
func systemstack(fn func())
switchtothread 说明
- 调用这个系统调用时,系统会查看一个迫切需要cpu时间的线程,如果没有就返回,SwitchToThread就对该线程进行调度(该线程的优先级可能低于调用SwitchToThread的线程)。这个迫切需要CPU时间的线程可以运行一个时间段,然后系统调度程序照常运行。
- 该函数允许一个需要资源的线程强制另一个优先级较低、而目前却拥有该资源的线程放弃该资源(抢占资源)。如果调用SwitchToThread函数时没有其他线程能够运行,那么该函数返回FALS E,否则返回一个非0值。
- SwitchToThread允许执行低优先级线程,Sleep会立即重新调度主调线程,即使低优先级线程会处于饥饿状态
updateTimerModifiedEarliest
- 修改p 记录最早运行时间的字段
// updateTimerModifiedEarliest updates the recorded nextwhen field of the
// earlier timerModifiedEarier value.
// The timers for pp will not be locked.
func updateTimerModifiedEarliest(pp *p, nextwhen int64) {
for {
old := atomic.Load64(&pp.timerModifiedEarliest)
if old != 0 && int64(old) < nextwhen {
return
}
if atomic.Cas64(&pp.timerModifiedEarliest, old, uint64(nextwhen)) {
return
}
}
}
resettimer
// resettimer resets the time when a timer should fire.
// If used for an inactive timer, the timer will become active.
// This should be called instead of addtimer if the timer value has been,
// or may have been, used previously.
// Reports whether the timer was modified before it was run.
func resettimer(t *timer, when int64) bool {
return modtimer(t, when, t.period, t.f, t.arg, t.seq)
}
- 调用修改timer ,将参数传进去
moveTimers
调用于C:\Go\src\runtime\proc.go+4926
- 将一个p 上的timer 移动到另一个p 上
- 被调用在STW上面,但是调用者期望pp 已经给timers 加锁
// moveTimers moves a slice of timers to pp. The slice has been taken
// from a different P.
// This is currently called when the world is stopped, but the caller
// is expected to have locked the timers for pp.
func moveTimers(pp *p, timers []*timer) {
for _, t := range timers { //循环遍历timer
loop:
for {
switch s := atomic.Load(&t.status); s {
case timerWaiting: //如果timer 是将运行的,将timer 状态置为移动
if !atomic.Cas(&t.status, s, timerMoving) {
continue
}
t.pp = 0 //状态改成功后,添加timer 也成功了,将状态重新置为待触发
doaddtimer(pp, t)
if !atomic.Cas(&t.status, timerMoving, timerWaiting) {
badTimer()
}
break loop
case timerModifiedEarlier, timerModifiedLater:
//如果timer 是时间被修改的,将timer 状态置为移动
if !atomic.Cas(&t.status, s, timerMoving) {
continue
}
t.when = t.nextwhen//更新时间
t.pp = 0//状态改成功后,添加timer 也成功了,将状态重新置为待触发
doaddtimer(pp, t)
if !atomic.Cas(&t.status, timerMoving, timerWaiting) {
badTimer()
}
break loop
case timerDeleted: //timer是已经被删除的将timer 状态置为移动
if !atomic.Cas(&t.status, s, timerRemoved) {
continue
}
t.pp = 0 但是pp 置为0
break loop
case timerModifying:
// Loop until the modification is complete.
osyield()
case timerNoStatus, timerRemoved:
badTimer()
case timerRunning, timerRemoving, timerMoving:
// Some other P thinks it owns this timer,
// which should not happen.
badTimer()
default:
badTimer()
}
}
}
}
- 总结来说就是先将状态置为待移除,然后重新添加到新p的堆上
应用
在p 被销毁时,将会调用这个函数将timer移动到其他p 上面去
func (pp *p) destroy() {
....
if len(pp.timers) > 0 {
plocal := getg().m.p.ptr()
//此时已经进入stw了,但是我们任然需要加锁去避免监控调用timeSleepUntil函数,发生在多个p 的情况下,所以不用担心死锁
lock(&plocal.timersLock)
lock(&pp.timersLock)
moveTimers(plocal, pp.timers) //将销毁的p 的timers 移动到当前执行goroutine 的p 上面
pp.timers = nil
pp.numTimers = 0
pp.deletedTimers = 0
atomic.Store64(&pp.timer0When, 0)
unlock(&pp.timersLock)
unlock(&plocal.timersLock)
}
.....
}
checkTimers
checkTimers 运行 P 准备好的任意一个timer
/usr/local/go/src/runtime/proc.go +3423
func checkTimers(pp *p, now int64) (rnow, pollUntil int64, ran bool) {
//加载第一个timer 什么时候执行,加载第一个timer 是否修改为更早的时间了
next := int64(atomic.Load64(&pp.timer0When))
nextAdj := int64(atomic.Load64(&pp.timerModifiedEarliest))
//如果next=0或者被修改为更早的时间了,则将next 修改为nextAdj
if next == 0 || (nextAdj != 0 && nextAdj < next) {
next = nextAdj
}
if next == 0 {
//没有修改或者重新修正timer
return now, 0, false
}
if now == 0 {
now = nanotime()
}
if now < next {
//当前时间小于下次执行的时间,timer 没有准备去运行,但是我们还是要继续去清理被删除的timers,
//下面的条件决定是否我们需要去清理timer
//timer 的p 不是当前g 运行的p或者删除的timer 数量没有超过总timer 数量的1/4,注意这里是单个p
if pp != getg().m.p.ptr() || int(atomic.Load(&pp.deletedTimers)) <= int(atomic.Load(&pp.numTimers)/4) {
return now, next, false
}
}
lock(&pp.timersLock) //加锁,
if len(pp.timers) > 0 {
adjusttimers(pp, now)
for len(pp.timers) > 0 {
// 注意runtimer 可能会短暂的解锁pp.timersLock.
if tw := runtimer(pp, now); tw != 0 { //进行runtimer 操作,如果tw 大于0,说明没有timer 可以执行,并且堆顶timer的下一次执行时间为tw
if tw > 0 {
pollUntil = tw
}
break
}
ran = true
}
}
//与上面条件相反,满足条件我们进进行清理
if pp == getg().m.p.ptr() && int(atomic.Load(&pp.deletedTimers)) > len(pp.timers)/4 {
clearDeletedTimers(pp)
}
unlock(&pp.timersLock)//解锁
return now, pollUntil, ran
}
- 先将堆顶的timer时间修改为正确的时间,因为timer 执行时间可能修改为更早了
- 判断是否满足清理被删除timer 的条件,如果不满足直接返回
- 如果p 上的timer数量大于0,不断的循环选择堆顶的timer,运行或者更新,如果没有可以执行的,将堆顶的timer 执行时间赋值给pollUntil
- 最后一步判断是否可以清理被删除timer ,如果是就清理timer,这里是让自己p 运行G处理自己上面的timer,可以减少锁竞争,提高效率
- 运行timer和清理timer都在锁里面进行的,这个锁的粒度是对P 进行加锁。
- now=0,会在这里获取当前时间赋值给now
runtimer
- 考察处理堆顶的元素,移除和更新堆顶的timer
- 返回0,如果timer 开始运行,返回-1 如果堆上没有timers.
- 调用者必须给pp(处理器) 上的timers加锁,如果timer 已经运行起来了,将会短暂的解锁
func runtimer(pp *p, now int64) int64 {
for {
t := pp.timers[0] //获取第0 个timer,堆顶的timer,根据状态做不同的操作
if t.pp.ptr() != pp {
throw("runtimer: bad p")
}
switch s := atomic.Load(&t.status); s { //原子获取到的timer 的状态
case timerWaiting: //状态代表准备去运行
if t.when > now {//但是t.when 大于当前时间,说明没有准备去运行,直接将下次执行时间返回
// Not ready to run.
return t.when
}
if !atomic.Cas(&t.status, s, timerRunning) { //将状态变为运行中,这个状态是短暂的,continue 原因是因为cas 操作可能失败,所以循环cas 操作
continue
}
runOneTimer(pp, t, now) //运行这个timer,会短暂的接口pp lock
return 0
case timerDeleted: //状态是被删除的,就将状态转为待移除
if !atomic.Cas(&t.status, s, timerRemoving) {
continue
}
dodeltimer0(pp) //删除p第一个timer
if !atomic.Cas(&t.status, timerRemoving, timerRemoved) {
badTimer()
}
atomic.Xadd(&pp.deletedTimers, -1)
if len(pp.timers) == 0 {
return -1
}
case timerModifiedEarlier, timerModifiedLater: //时间被修改成更早或者更晚,这时候需要删除堆顶的timer
if !atomic.Cas(&t.status, s, timerMoving) {
continue
}
t.when = t.nextwhen //将时间更新
dodeltimer0(pp)//删除堆顶的timer,也就是之前的自己
doaddtimer(pp, t)//重新将timer 添加进去
if !atomic.Cas(&t.status, timerMoving, timerWaiting) {//状态改为等待待触发,
badTimer()
}
case timerModifying:
// Wait for modification to complete.
osyield()
case timerNoStatus, timerRemoved: //状态是错误的,不活跃的timer 不会出现在堆上
badTimer()
case timerRunning, timerRemoving, timerMoving:
// These should only be set when timers are locked,
//这些状态只会出现在timer 被locked 时候
badTimer()
default:
badTimer()
}
}
}
runOneTimer
- 运行单个timer
- 调用者必须锁住p 的timers
- 将会短暂的解开timers 的锁在执行函数的时候
func runOneTimer(pp *p, t *timer, now int64) {
if raceenabled {
ppcur := getg().m.p.ptr()
if ppcur.timerRaceCtx == 0 {
ppcur.timerRaceCtx = racegostart(funcPC(runtimer) + sys.PCQuantum)
}
raceacquirectx(ppcur.timerRaceCtx, unsafe.Pointer(t))
}
f := t.f //获取timer执行函数
arg := t.arg//获取timer 参数
seq := t.seq//获取timer 参数
if t.period > 0 { //代表这是周期性任务,时间间隔为t.period
// Leave in heap but adjust next time to fire.
delta := t.when - now
t.when += t.period * (1 + -delta/t.period)
if t.when < 0 { // check for overflow.
t.when = maxWhen
}
siftdownTimer(pp.timers, 0) //堆算法下滤操作,将timer 从timerRunning状态重新置为timerWaiting状态
if !atomic.Cas(&t.status, timerRunning, timerWaiting) {
badTimer()
}
updateTimer0When(pp)
} else {
// 不是周期性任务,删掉timer,然后将timer 的状态改为无状态,等待被回收,不得不说go 官方还是挺仔细的,要是我写返回会被回收,估计就不会去重置timer状态了
dodeltimer0(pp)
if !atomic.Cas(&t.status, timerRunning, timerNoStatus) {
badTimer()
}
}
if raceenabled {
// Temporarily use the current P's racectx for g0.
gp := getg()
if gp.racectx != 0 {
throw("runOneTimer: unexpected racectx")
}
gp.racectx = gp.m.p.ptr().timerRaceCtx
}
unlock(&pp.timersLock)
f(arg, seq)
lock(&pp.timersLock)
if raceenabled {
gp := getg()
gp.racectx = 0
}
}
- 该函数运行一个timer,如果是周期性任务,先将原来的timer 从堆里面删除掉,然后更新原来的timer时间为下一次运行时间,重新插入堆中。如果是单任务则直接删掉
- 下面将参数和回调函数获取到,然后直接执行,这里有个细节点,就是解除了p 的锁,其实这也是官方的优化,如果不解除只能保证本p 上的G 运行这个任务,到这里已经不涉及到任何数据竞争了,只是单纯的执行回调函数,让其他p 上的 去抢占执行才是最正确的选择。要不然将会一直占用timer 所属p 的资源。
adjusttimers
- 查找当前p 上的timers 是否有被修改为更早时间的,找到把它放在堆上正确的位置
- 它也移动将会修改到后面运行的timers ,或者移除被删除的timers
- 操作必须加锁
func adjusttimers(pp *p, now int64) {
// If we haven't yet reached the time of the first timerModifiedEarlier
// timer, don't do anything. This speeds up programs that adjust
// a lot of timers back and forth if the timers rarely expire.
// We'll postpone looking through all the adjusted timers until
// one would actually expire.
first := atomic.Load64(&pp.timerModifiedEarliest)
if first == 0 || int64(first) > now {
if verifyTimers {
verifyTimerHeap(pp)
}
return
}
// We are going to clear all timerModifiedEarlier timers.
atomic.Store64(&pp.timerModifiedEarliest, 0)
var moved []*timer
for i := 0; i < len(pp.timers); i++ { //循环遍历该p 上的所有timer
t := pp.timers[i]
if t.pp.ptr() != pp {
throw("adjusttimers: bad p")
}
switch s := atomic.Load(&t.status); s {
case timerDeleted: //是删除的话,将状态改为将移除
if atomic.Cas(&t.status, s, timerRemoving) {
changed := dodeltimer(pp, i) //删除该位置的timer
if !atomic.Cas(&t.status, timerRemoving, timerRemoved) { //将状态改为已移除
badTimer()
}
atomic.Xadd(&pp.deletedTimers, -1) //已标记删掉的timers -1
// 在for 循环里面删掉了这个timer,此时的i 会变了,获取正确位置的i
i = changed - 1
}
case timerModifiedEarlier, timerModifiedLater:
if atomic.Cas(&t.status, s, timerMoving) {
//将timer 执行时间修改为正确的执行时间
t.when = t.nextwhen
//将timer 从堆里面拿起来,我们不在这里做是因为添加会改变timer 在堆里面位置,在遍历完在重新添加。
changed := dodeltimer(pp, i)
moved = append(moved, t)
//获取正确的i 索引
i = changed - 1
}
case timerNoStatus, timerRunning, timerRemoving, timerRemoved, timerMoving:
badTimer()
case timerWaiting:
// OK, nothing to do.
case timerModifying:
// Check again after modification is complete.
osyield()//等一下
i-- //将i-- ,重复上一步操作
default:
badTimer()
}
}
if len(moved) > 0 {
addAdjustedTimers(pp, moved)
}
if verifyTimers {
verifyTimerHeap(pp)
}
}
//循环的将timer 的重新添加到p 的堆里面去,并将timer 的状态改为正在等待触发
func addAdjustedTimers(pp *p, moved []*timer) {
for _, t := range moved {
doaddtimer(pp, t)
if !atomic.Cas(&t.status, timerMoving, timerWaiting) {
badTimer()
}
}
}
verifyTimerHeap
- 在调试状态下,verifyTimers为将被设置为true,然后就会验证堆的顺序
func verifyTimerHeap(pp *p) {
for i, t := range pp.timers { //遍历所有timer
if i == 0 {
// 堆顶timer 直接跳过
continue
}
//这个是4叉堆,所以检查parent的是否大于自己的时间,如果不是panic
// The heap is 4-ary. See siftupTimer and siftdownTimer.
p := (i - 1) / 4
if t.when < pp.timers[p].when {
print("bad timer heap at ", i, ": ", p, ": ", pp.timers[p].when, ", ", i, ": ", t.when, "\n")
throw("bad timer heap")
}
}
if numTimers := int(atomic.Load(&pp.numTimers)); len(pp.timers) != numTimers {
println("timer heap len", len(pp.timers), "!= numTimers", numTimers)
throw("bad timer heap len")
}
}
checkTimers触发
stealWork
go/src/runtime/proc.go +3015
作用是:从其他p 偷取可运行的timer 或者goroutine,这里只讨论timer
- 这是唯一调用checkTimers 我们必须加锁处理其他p 的timers 的地方
- timerpMask 告诉我们p 是否有timers ,如果没有的话我们就不用一直check
func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool){
................
if stealTimersOrRunNextG && timerpMask.read(enum.position()) {
tnow, w, ran := checkTimers(p2, now)
now = tnow
if w != 0 && (pollUntil == 0 || w < pollUntil) {
pollUntil = w
}
if ran {
if gp, inheritTime := runqget(pp); gp != nil {
return gp, inheritTime, now, pollUntil, ranTimer
}
ranTimer = true
}
}
................
}
findrunnable
该函数作用是寻找可运行的goroutine,会触发timer
go/src/runtime/proc.go +2705
// Finds a runnable goroutine to execute.
// Tries to steal from other P's, get g from local or global queue, poll network.
func findrunnable() (gp *g, inheritTime bool) {
....
now, pollUntil, _ := checkTimers(_p_, 0)
.....
}
schedule
一种是在调度循环的时候调用 checkTimers
方法进行计时器的触发
go/src/runtime/proc.go +3291
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule(
.....
top:
pp := _g_.m.p.ptr()
pp.preempt = false
if sched.gcwaiting != 0 {
gcstopm()
goto top
}
if pp.runSafePointFn != 0 {
runSafePointFn()
}
// Sanity check: if we are spinning, the run queue should be empty.
// Check this before calling checkTimers, as that might call
// goready to put a ready goroutine on the local run queue.
if _g_.m.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
throw("schedule: spinning with local work")
}
checkTimers(pp, 0)
.....
)
sysmon
Go runtime 在程序启动的时候会创建一个独立的 M 作为监控线程,叫 sysmon
,这个线程为系统级的 daemon 线程,无需 P 即可运行, sysmon
每 20us~10ms 运行一次。
每次调度器调度和窃取的时候触发,但毕竟是具有一定的随机和不确定性。系统监控触发依然是一个兜底保障,那就是runtime.sysmon 监控线程
func sysmon(){
.....
next, _ := timeSleepUntil()
......
}
timeSleepUntil
返回最近将会执行的timer,这里会遍历所有的p ,获取最先执行的timer 时间并返回
func timeSleepUntil() (int64, *p) {
next := int64(maxWhen)
var pret *p
// Prevent allp slice changes. This is like retake.
lock(&allpLock)
for _, pp := range allp {
if pp == nil {
// This can happen if procresize has grown
// allp but not yet created new Ps.
continue
}
w := int64(atomic.Load64(&pp.timer0When))
if w != 0 && w < next {
next = w
pret = pp
}
w = int64(atomic.Load64(&pp.timerModifiedEarliest))
if w != 0 && w < next {
next = w
pret = pp
}
}
unlock(&allpLock)
return next, pret
}
4 常用使用案例
4.1超时控制
package main
import (
"fmt"
"time"
)
func main() {
t:=time.NewTimer(1*time.Second)
defer t.Stop()
go func() {
select {
case <-doWork():
fmt.Println("正常退出")
return
case <-t.C:
fmt.Println("超时")
return
}
}()
time.Sleep(5*time.Second)
}
func doWork()chan struct{}{
var ch =make(chan struct{})
go func() {
time.Sleep(2*time.Second) //模拟超时
ch<- struct{}{}
}()
return ch
}
4.2 错误导致的内存泄漏
翻车示例,在之前的项目框架里面看到过,幸好timer内存泄漏容易排查,不容易排查的话,上线出现问题可能就会被拖出去祭天了(#`O′)。下面是一个内存泄露的例子,for + Select + After 系列
package main
import (
"log"
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
ch := make(chan int, 10)
go func() {
var i = 1
for {
i++
ch <- i
}
}()
for {
select {
case x := <-ch:
println(x)
case <-time.After(3 * time.Minute):
println(time.Now().Unix())
}
}
}
输入调试命令:
如果查看分配内存可以用
go tool pprof -alloc_space http://127.0.0.1:6060/debug/pprof/heap
查看使用内存命令
➜ awesomeProject1 go tool pprof -inuse_space http://127.0.0.1:6060/debug/pprof/heap
Fetching profile over HTTP from http://127.0.0.1:6060/debug/pprof/heap
Saved profile in /Users/wuming/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.008.pb.gz
Type: inuse_space
Time: Nov 7, 2021 at 1:34pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) web //图形界面
(pprof) top //查看内存占用最多项
Showing nodes accounting for 171.92MB, 99.42% of 172.92MB total
Dropped 6 nodes (cum <= 0.86MB)
Showing top 10 nodes out of 14
flat flat% sum% cum cum%
0 0% 0% 169.92MB 98.26% main.main
0 0% 0% 169.92MB 98.26% runtime.main
0 0% 0% 169.92MB 98.26% time.After (inline)
162.01MB 93.69% 93.69% 169.92MB 98.26% time.NewTimer //这个函数占用了大部分内存
7.91MB 4.57% 98.26% 7.91MB 4.57% time.startTimer
2MB 1.16% 99.42% 2MB 1.16% runtime.allocm
0 0% 99.42% 2MB 1.16% runtime.mstart
0 0% 99.42% 2MB 1.16% runtime.mstart0
0 0% 99.42% 2MB 1.16% runtime.mstart1
0 0% 99.42% 2MB 1.16% runtime.newm
(pprof)
图形界面查看示例
原理在前面已经讲过,time.After(3 * time.Minute) 每次select都会执行一次,创建timer,但是没有调用stop 去标记被删除,timer就只有超时以后才会被标记删除,然后被gc 回收,如果超时时间比较长,再加上for 组合,就会一直创建timer,可谓是锅从天上来(#`O′)。
参考:
go 专家编程