Go time.NewTicker()与定时器

Go 1.14为了优化计时器的性能,将计时器和epoll耦合在一起了~

创建一个timer时,也会进到epoll的初始化(新timer的实现依赖了epoll的实现)

现在的timer实现有点复杂了~

一次性定时器和周期性定时器




n个定时器需要n个协程吗?


即是否每个定时器,都需要后台有一个协程来专门维护?? 如不如此,是如何做到的?


参考 Go语言中的定时器–简书,验证如下:

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
package main

import (
"fmt"
"os"
"runtime/debug"
"time"
)

func main() {
debug.SetTraceback("system")

fmt.Println("os.Args:", os.Args)
fmt.Println("len(os.Args):", len(os.Args))

// 如果不带参数执行,会在定时器之前就panic,并打印出所有goroutine
if len(os.Args) == 1 {
// "不用panic的话,就没有什么简单的方法来查看运行时的goroutine ---- 它们被runtime.NumGoroutines和runtime.Stack排除了,所以仅剩的方法就是让程序crash掉"
panic("before timers")
}

// 如果带有参数,则会打印出生成1万个定时器之后的所有goroutine
for i := 0; i < 10000; i++ {
time.AfterFunc(5*time.Second, func() {
fmt.Println("Hello!", i)
})
}
panic("after timers")
}

如果不带参数执行,即go run xxx.go:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
os.Args: [/var/folders/9t/839s3jmj73bcgyp5x_xh3gw00000gn/T/go-build3283038849/b001/exe/panic]
len(os.Args): 1
panic: before timers

goroutine 1 [running]:
panic(0x1007730c0, 0x10078ce68)
/usr/local/go/src/runtime/panic.go:1065 +0x4d8 fp=0x1400011fed0 sp=0x1400011fe00 pc=0x1006de848
main.main()
/Users/fliter/go/src/shuang/ticker/panic.go:19 +0x22c fp=0x1400011ff70 sp=0x1400011fed0 pc=0x1007466fc
runtime.main()
/usr/local/go/src/runtime/proc.go:225 +0x26c fp=0x1400011ffd0 sp=0x1400011ff70 pc=0x1006e104c
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x1400011ffd0 sp=0x1400011ffd0 pc=0x10070e5e4

goroutine 2 [force gc (idle)]:
runtime.gopark(0x10078cb20, 0x100805f00, 0x1411, 0x1)
/usr/local/go/src/runtime/proc.go:336 +0xd0 fp=0x14000042fa0 sp=0x14000042f80 pc=0x1006e1450
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:342
runtime.forcegchelper()
/usr/local/go/src/runtime/proc.go:276 +0xbc fp=0x14000042fd0 sp=0x14000042fa0 pc=0x1006e12dc
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x14000042fd0 sp=0x14000042fd0 pc=0x10070e5e4
created by runtime.init.6
/usr/local/go/src/runtime/proc.go:264 +0x30

goroutine 3 [GC sweep wait]:
runtime.gopark(0x10078cb20, 0x100806020, 0x140c, 0x1)
/usr/local/go/src/runtime/proc.go:336 +0xd0 fp=0x140000437a0 sp=0x14000043780 pc=0x1006e1450
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:342
runtime.bgsweep(0x14000026070)
/usr/local/go/src/runtime/mgcsweep.go:163 +0xb0 fp=0x140000437d0 sp=0x140000437a0 pc=0x1006cee30
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x140000437d0 sp=0x140000437d0 pc=0x10070e5e4
created by runtime.gcenable
/usr/local/go/src/runtime/mgc.go:217 +0x54

goroutine 4 [GC scavenge wait]:
runtime.gopark(0x10078cb20, 0x1008060c0, 0x140d, 0x1)
/usr/local/go/src/runtime/proc.go:336 +0xd0 fp=0x14000043f70 sp=0x14000043f50 pc=0x1006e1450
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:342
runtime.bgscavenge(0x14000026070)
/usr/local/go/src/runtime/mgcscavenge.go:265 +0xec fp=0x14000043fd0 sp=0x14000043f70 pc=0x1006cd24c
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x14000043fd0 sp=0x14000043fd0 pc=0x10070e5e4
created by runtime.gcenable
/usr/local/go/src/runtime/mgc.go:218 +0x74

goroutine 5 [finalizer wait]:
runtime.gopark(0x10078cb20, 0x100833310, 0x14000021410, 0x1)
/usr/local/go/src/runtime/proc.go:336 +0xd0 fp=0x14000042730 sp=0x14000042710 pc=0x1006e1450
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:342
runtime.runfinq()
/usr/local/go/src/runtime/mfinal.go:175 +0xb8 fp=0x140000427d0 sp=0x14000042730 pc=0x1006c4a48
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x140000427d0 sp=0x140000427d0 pc=0x10070e5e4
created by runtime.createfing
/usr/local/go/src/runtime/mfinal.go:156 +0x78
exit status 2


如果带参数执行,即go run xxx.go -666:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
os.Args: [/var/folders/9t/839s3jmj73bcgyp5x_xh3gw00000gn/T/go-build466442962/b001/exe/panic -123]
len(os.Args): 2
panic: after timers

goroutine 1 [running]:
panic(0x104e6f0c0, 0x104e88e78)
/usr/local/go/src/runtime/panic.go:1065 +0x4d8 fp=0x14000096ed0 sp=0x14000096e00 pc=0x104dda848
main.main()
/Users/fliter/go/src/shuang/ticker/panic.go:28 +0x210 fp=0x14000096f70 sp=0x14000096ed0 pc=0x104e426e0
runtime.main()
/usr/local/go/src/runtime/proc.go:225 +0x26c fp=0x14000096fd0 sp=0x14000096f70 pc=0x104ddd04c
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x14000096fd0 sp=0x14000096fd0 pc=0x104e0a5e4

goroutine 2 [force gc (idle)]:
runtime.gopark(0x104e88b20, 0x104f01f00, 0x1411, 0x1)
/usr/local/go/src/runtime/proc.go:336 +0xd0 fp=0x14000042fa0 sp=0x14000042f80 pc=0x104ddd450
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:342
runtime.forcegchelper()
/usr/local/go/src/runtime/proc.go:276 +0xbc fp=0x14000042fd0 sp=0x14000042fa0 pc=0x104ddd2dc
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x14000042fd0 sp=0x14000042fd0 pc=0x104e0a5e4
created by runtime.init.6
/usr/local/go/src/runtime/proc.go:264 +0x30

goroutine 17 [GC sweep wait]:
runtime.gopark(0x104e88b20, 0x104f02020, 0x140c, 0x1)
/usr/local/go/src/runtime/proc.go:336 +0xd0 fp=0x1400003e7a0 sp=0x1400003e780 pc=0x104ddd450
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:342
runtime.bgsweep(0x1400008c000)
/usr/local/go/src/runtime/mgcsweep.go:163 +0xb0 fp=0x1400003e7d0 sp=0x1400003e7a0 pc=0x104dcae30
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x1400003e7d0 sp=0x1400003e7d0 pc=0x104e0a5e4
created by runtime.gcenable
/usr/local/go/src/runtime/mgc.go:217 +0x54

goroutine 18 [GC scavenge wait]:
runtime.gopark(0x104e88b20, 0x104f020c0, 0x140d, 0x1)
/usr/local/go/src/runtime/proc.go:336 +0xd0 fp=0x1400003ef70 sp=0x1400003ef50 pc=0x104ddd450
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:342
runtime.bgscavenge(0x1400008c000)
/usr/local/go/src/runtime/mgcscavenge.go:265 +0xec fp=0x1400003efd0 sp=0x1400003ef70 pc=0x104dc924c
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x1400003efd0 sp=0x1400003efd0 pc=0x104e0a5e4
created by runtime.gcenable
/usr/local/go/src/runtime/mgc.go:218 +0x74

goroutine 3 [finalizer wait]:
runtime.gopark(0x104e88b20, 0x104f2f310, 0x1410, 0x1)
/usr/local/go/src/runtime/proc.go:336 +0xd0 fp=0x14000043730 sp=0x14000043710 pc=0x104ddd450
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:342
runtime.runfinq()
/usr/local/go/src/runtime/mfinal.go:175 +0xb8 fp=0x140000437d0 sp=0x14000043730 pc=0x104dc0a48
runtime.goexit()
/usr/local/go/src/runtime/asm_arm64.s:1130 +0x4 fp=0x140000437d0 sp=0x140000437d0 pc=0x104e0a5e4
created by runtime.createfing
/usr/local/go/src/runtime/mfinal.go:156 +0x78
exit status 2


两次都是5个goroutine


实际上,亲测出的结果,即便新增了1万个定时器,也没有新增一个goroutine

以上代码在基于amd64的Mac M1上执行,go版本为1.16


使用amd64的机器,go 1.11上运行,则符合原文预期,即不起定时器前4个goroutine,之后5个goroutine:

1
2
3
go run xxxx.go 2>&1 | grep "^goroutine" | wc -l

4
1
2
3
go run xxxx.go -123 2>&1 | grep "^goroutine" | wc -l

5

直接run,具体输出如下:

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
os.Args: [/tmp/go-build548947945/b001/exe/ticker]
len(os.Args): 1
panic: before timers

goroutine 1 [running]:
panic(0x49f060, 0x4d3ee0)
/usr/local/go/src/runtime/panic.go:556 +0x2cb fp=0xc00006af10 sp=0xc00006ae80 pc=0x4270db
main.main()
/home/ubuntu/go_lab/ticker.go:19 +0x1fd fp=0xc00006af98 sp=0xc00006af10 pc=0x48ea5d
runtime.main()
/usr/local/go/src/runtime/proc.go:201 +0x207 fp=0xc00006afe0 sp=0xc00006af98 pc=0x428e07
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc00006afe8 sp=0xc00006afe0 pc=0x4515c1

goroutine 2 [runnable]:
runtime.forcegchelper()
/usr/local/go/src/runtime/proc.go:243 fp=0xc000032fe0 sp=0xc000032fd8 pc=0x428fb0
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc000032fe8 sp=0xc000032fe0 pc=0x4515c1
created by runtime.init.4
/usr/local/go/src/runtime/proc.go:240 +0x35

goroutine 3 [GC sweep wait]:
runtime.gopark(0x4c7058, 0x55cfa0, 0x41140c, 0x1)
/usr/local/go/src/runtime/proc.go:302 +0xeb fp=0xc000033780 sp=0xc000033760 pc=0x4291eb
runtime.goparkunlock(0x55cfa0, 0x4d140c, 0x1)
/usr/local/go/src/runtime/proc.go:308 +0x53 fp=0xc0000337b0 sp=0xc000033780 pc=0x429293
runtime.bgsweep(0xc00004a000)
/usr/local/go/src/runtime/mgcsweep.go:52 +0x8f fp=0xc0000337d8 sp=0xc0000337b0 pc=0x41cc8f
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc0000337e0 sp=0xc0000337d8 pc=0x4515c1
created by runtime.gcenable
/usr/local/go/src/runtime/mgc.go:216 +0x58

goroutine 4 [runnable]:
runtime.runfinq()
/usr/local/go/src/runtime/mfinal.go:161 fp=0xc000033fe0 sp=0xc000033fd8 pc=0x414700
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc000033fe8 sp=0xc000033fe0 pc=0x4515c1
created by runtime.createfing
/usr/local/go/src/runtime/mfinal.go:156 +0x61
exit status 2


带参数之后:

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
44
45
46
47
48
49
50
os.Args: [/tmp/go-build829376033/b001/exe/ticker -111]
len(os.Args): 2
panic: after timers

goroutine 1 [running]:
panic(0x49f060, 0x4d3ef0)
/usr/local/go/src/runtime/panic.go:556 +0x2cb fp=0xc00006af10 sp=0xc00006ae80 pc=0x4270db
main.main()
/home/ubuntu/go_lab/ticker.go:28 +0x1df fp=0xc00006af98 sp=0xc00006af10 pc=0x48ea3f
runtime.main()
/usr/local/go/src/runtime/proc.go:201 +0x207 fp=0xc00006afe0 sp=0xc00006af98 pc=0x428e07
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc00006afe8 sp=0xc00006afe0 pc=0x4515c1

goroutine 2 [runnable]:
runtime.forcegchelper()
/usr/local/go/src/runtime/proc.go:243 fp=0xc000030fe0 sp=0xc000030fd8 pc=0x428fb0
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc000030fe8 sp=0xc000030fe0 pc=0x4515c1
created by runtime.init.4
/usr/local/go/src/runtime/proc.go:240 +0x35

goroutine 3 [GC sweep wait]:
runtime.gopark(0x4c7058, 0x55cfa0, 0x41140c, 0x1)
/usr/local/go/src/runtime/proc.go:302 +0xeb fp=0xc000031780 sp=0xc000031760 pc=0x4291eb
runtime.goparkunlock(0x55cfa0, 0x4d140c, 0x1)
/usr/local/go/src/runtime/proc.go:308 +0x53 fp=0xc0000317b0 sp=0xc000031780 pc=0x429293
runtime.bgsweep(0xc00004a000)
/usr/local/go/src/runtime/mgcsweep.go:52 +0x8f fp=0xc0000317d8 sp=0xc0000317b0 pc=0x41cc8f
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc0000317e0 sp=0xc0000317d8 pc=0x4515c1
created by runtime.gcenable
/usr/local/go/src/runtime/mgc.go:216 +0x58

goroutine 4 [runnable]:
runtime.runfinq()
/usr/local/go/src/runtime/mfinal.go:161 fp=0xc000031fe0 sp=0xc000031fd8 pc=0x414700
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc000031fe8 sp=0xc000031fe0 pc=0x4515c1
created by runtime.createfing
/usr/local/go/src/runtime/mfinal.go:156 +0x61

goroutine 5 [runnable]:
runtime.timerproc(0x55fb80)
/usr/local/go/src/runtime/time.go:224 fp=0xc0000307d8 sp=0xc0000307d0 pc=0x442d00
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc0000307e0 sp=0xc0000307d8 pc=0x4515c1
created by runtime.(*timersBucket).addtimerLocked
/usr/local/go/src/runtime/time.go:170 +0x114
exit status 2

原文中使用的命令如下:

go run xxx.go 2>&1 | grep “^goroutine” | wc -l

https://studygolang.com/articles/21597


https://www.cnblogs.com/qggg/p/8571808.html

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-timer/

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-netpoller/

http://xiaorui.cc/archives/6483

https://www.google.com.hk/search?q=site:http://xiaorui.cc/+%E5%AE%9A%E6%97%B6%E5%99%A8&newwindow=1&sxsrf=AOaemvIfh6H5ii2THKYpt0lZfLNlN_ULIg:1635769019251&ei=u9p_YezPDtGTr7wPgPGdmA4&start=10&sa=N&ved=2ahUKEwismc66kvfzAhXRyYsBHYB4B-MQ8tMDegQIARA0&biw=1800&bih=993&dpr=1.6

https://www.zhihu.com/zvideo/1350812665179152384

https://www.zhihu.com/question/424920760/answer/1520371371

https://www.zhihu.com/question/424920760/answer/1520371371

https://zhuanlan.zhihu.com/p/423934269

https://zhuanlan.zhihu.com/p/338356039

https://www.bilibili.com/video/BV1rK411N7M3

https://xargin.com/go-timer/

https://www.jianshu.com/p/427dfe8ad3c0

https://juejin.cn/post/6884914839308533774#heading-2

https://studygolang.com/articles/5224

https://mp.weixin.qq.com/s/QahprdKrlcaatG8poWsNrA

https://mp.weixin.qq.com/s/gaQhIo544VHYeGcKq34GIw

https://mp.weixin.qq.com/s/i8XWQgsgySIneZ6G_GPzuw

https://mp.weixin.qq.com/s/yYYgOfzwknh53yqBc3vL0Q

https://mp.weixin.qq.com/s/bV097lsHfdIaJFLYjaYYFA

cuishuangtodo

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

1.14版本之前是使用 64 个最小堆,运行时创建的所有计时器都会加入到最小堆中,每个处理器(P)创建的计时器会由对应的最小堆维护

在Go 在1.14版本之后,移除了timersBucket,所有的计时器都以最小四叉堆的形式存储 P 中。(所以不需要加锁)。timer 不再使用 timerproc 异步任务来调度,而是改用调度循环或系统监控调度的时候进行触发执行,减少了线程之间上下文切换带来的性能损失,并且通过使用 netpoll 阻塞唤醒机制可以让 timer 更加及时的得到执行。

timer 的触发有两种:

  • 从调度循环中直接触发;

  • 另一种是Go语言的后台系统监控中会定时触发;

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

Go语言中时间轮的实现