上一章中对于golang的常用关键字说明如下:
接下来我们来对golang的并发编程进行说明,主要内容有:3 定时器
4 Channel
5 调度器
6 网络轮询器
7 系统监控
— — — — — — — — — — — — — — — — — — — — — — — — — — — —
准确的时间对于任何一个正在运行的应用非常重要,但是在一个分布式系统中我们很难保证各个节点上绝对时间的一致,哪怕通过 NTP 这种标准的对时协议也只能把各个节点上时间的误差控制在毫秒级,所以准确的相对时间在分布式系统中显得更为重要,本节会分析用于获取相对时间的计时器(Timer)的设计与实现原理。
3.1 设计原理
Go 语言从实现计时器到现在经历过很多个版本的迭代,到最新的 1.14 版本为止,计时器的实现分别经历了以下几个过程:Go 1.9 版本之前,所有的计时器由全局唯一的四叉堆维护1;
Go 1.10 ~ 1.13,全局使用 64 个四叉堆维护全部的计时器,每个处理器(P)创建的计时器会由对应的四叉堆维护2;
Go 1.14 版本之后,每个处理器单独管理计时器并通过网络轮询器触发3;
我们在这一节会分别介绍计时器在不同版本的不同设计,梳理计时器实现的演进过程。
全局四叉堆
Go 1.10 之前的计时器都使用最小四叉堆实现,所有的计时器都会存储在如下所示的结构体
var timers struct {
lock mutex
gp *g
created bool
sleeping bool
rescheduling bool
sleepUntil int64
waitnote note
t []*timer
}
这个结构体中的字段 t 就是最小四叉堆,创建的所有计时器都会加入到四叉堆中。四叉堆中的计时器到期;
四叉堆中加入了触发时间更早的新计时器;
图 - 计时器四叉堆
然而全局四叉堆共用的互斥锁对计时器的影响非常大,计时器的各种操作都需要获取全局唯一的互斥锁,这会严重影响计时器的性能4。
分片四叉堆
Go 1.10 将全局的四叉堆分割成了 64 个更小的四叉堆5。在理想情况下,四叉堆的数量应该等于处理器的数量,但是这需要实现动态的分配过程,所以经过权衡最终选择初始化 64 个四叉堆,以牺牲内存占用的代价换取性能的提升。
const timersLen = 64
var timers [timersLen]struct {
timersBucket
}
type timersBucket struct {
lock mutex
gp *g
created bool
sleeping bool
rescheduling bool
sleepUntil int64
waitnote note
t []*timer
}
如果当前机器上的处理器 P 的个数超过了 64,多个处理器上的计时器就可能存储在同一个桶中。每一个计时器桶都由一个运行
图 - 分片计时器桶
将全局计时器分片的方式,虽然能够降低锁的粒度,提高计时器的性能,但是 6。
网络轮询器
在最新版本的实现中,计时器桶已经被移除7,所有的计时器都以最小四叉堆的形式存储在处理器
图 - 处理器中的最小四叉堆
处理器timersLock — 用于保护计时器的互斥锁;
timers — 存储计时器的最小四叉堆;
numTimers — 处理器中的计时器数量;
adjustTimers — 处理器中处于 timerModifiedEarlier 状态的计时器数量;
deletedTimers — 处理器中处于 timerDeleted 状态的计时器数量;
type p struct {
...
timersLock mutex
timers []*timer
numTimers uint32
adjustTimers uint32
deletedTimers uint32
...
}
原本用于管理计时器的
3.2 数据结构
type timer struct {
pp puintptr
when int64
period int64
f func(interface{}, uintptr)
arg interface{}
seq uintptr
nextwhen int64
status uint32
}when — 当前计时器被唤醒的时间;
period — 两次被唤醒的间隔;
f — 每当计时器被唤醒时都会调用的函数;
arg — 计时器被唤醒时调用 f 传入的参数;