Context
在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用
- context.Background
- context.TODO
- context.WithDeadline
- context.WithValue
同步原语与锁
sync.Mutex
sync.RWMutex
sync.WaitGroup
sync.Once
sync.Cond
CAS(compare and swap)
是一条cpu的原子指令, cpu进行比较两个值是否相等, 然后原子的更新某个位置的值.
sync.Mutex 互斥锁
-
正常模式:
锁的等待者会按照先进先出的顺序获取锁 -
饥饿模式:
一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 被『饿死』。互斥锁会直接交给等待队列最前面的 Goroutine。
在获取锁时如果已经被锁了,就会通过自旋等方式等待锁的释放.
自旋期间会一直占用cpu时间
RWMutex 读写互斥锁
细粒度的互斥锁, 不限制读的并发, 但是读写, 写写操作无法并发.
写操作:
sync.RWMutex.Lock 和 sync.RWMutex.Unlock
加锁
- 阻塞后续的写操作, 其他Goroutine在获取写锁是会进入自旋或休眠
- 阻塞后续读操作
- 如果还有Goroutine持有读锁, 当前Goroutine会进入休眠, 等待所有读锁释放后通过信号量将其唤醒.
解锁
- 释放读锁
- 释放所有因获取读锁而陷入等待的Goroutine
- 释放写锁
读操作:
sync.RWMutex.RLock 和 sync.RWMutex.RUnlock
加锁
- 如果有其他Goroutine获得了写锁, 那么久陷入休眠等待锁释放
- 没有Goroutine获得写锁, 获取读锁
解锁
- 没有其他Goroutine获取写锁, 释放读锁
- 如果有其他Goroutine持有写锁, 释放读锁,如果是最后一个持有读锁的会触发信号量, 调度器会唤醒尝试获取写锁的Goroutine
WaitGroup
sync.WaitGroup 可以等待一组 Goroutine 的返回
sync.Once
保证在 Go 程序运行期间的某段代码只会执行一次
sync.Cond
可以让一组的 Goroutine 都在满足特定条件时被唤醒
ErrGroup
为一组 Goroutine 中提供了同步、错误传播以及上下文取消的功能
- 只有第一个错误会被返回, 剩余的错误会被丢弃
Semaphore 信号量
获取资源
semaphore.Weighted.Acquire 阻塞
semaphore.Weighted.TryAcquire 非阻塞
释放资源
semaphore.Weighted.Release
SingleFlight
在一个服务中抑制对下游的多次重复请求(如redis缓存击穿)
定时器
time.Timer
轮询力度10ms
Channel
- 先从 Channel 读取数据的 Goroutine 会先接收到数据
- 先向 Channel 发送数据的 Goroutine 会得到先发送数据的权利
type hchan struct {
qcount uint // channel 中元素个数
dataqsiz uint // 循环队列的长度
buf unsafe.Pointer // 缓冲区数据指针
elemsize uint16 // 收发的元素大小
closed uint32
elemtype *_type // 收发的元素类型
sendx uint // 发送操作处理到的位置
recvx uint // 接收操作处理到的位置
recvq waitq // 阻塞的接收Goroutine 列表
sendq waitq // 阻塞的发送Goroutine 列表
lock mutex
}
发送数据
发送数据对当前channel加锁, 防止并发修改数据
- 如果当前 Channel 的 recvq 上存在已经被阻塞的 Goroutine,那么会直接将数据发送给当前 Goroutine 并将其设置成下一个运行的 Goroutine
- 如果 Channel 存在缓冲区并且其中还有空闲的容量,我们会直接将数据存储到缓冲区 sendx 所在的位置上
- 如果不满足上面的两种情况,会创建一个 runtime.sudog 结构并将其加入 Channel 的 sendq 队列中,当前 Goroutine 也会陷入阻塞等待其他的协程从 Channel 接收数据
接收数据
- 如果 Channel 为空,那么会直接调用 runtime.gopark 挂起当前 Goroutine
- 如果 Channel 已经关闭并且缓冲区没有任何数据,runtime.chanrecv 会直接返回
- 如果 Channel 的 sendq 队列中存在挂起的 Goroutine,会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区
- 如果 Channel 的缓冲区中包含数据,那么直接读取 recvx 索引对应的数据
- 在默认情况下会挂起当前的 Goroutine,将 runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒
关闭管道
当 Channel 是一个空指针或者已经被关闭时, 会panic
调度器
任务窃取(编译器在函数调用时插入函数)
抢占式调度(gc时基于信号量)
G-M-P 模型
- G 表示Goroutine,它是一个待执行的任务
- M — 表示操作系统的线程,它由操作系统的调度器调度和管理
- P — 表示处理器,它可以被看做运行在线程上的本地调度器
G
// 部分字段
type g struct {
stack stack // 栈内存范围
stackguard0 uintptr // 用于调度器抢占式调度
preempt bool // 抢占信号
preemptStop bool // 抢占时将状态修改成 `_Gpreempted`
preemptShrink bool // 在同步安全点收缩栈
_panic *_panic // 最内侧的 panic 结构体
_defer *_defer // 最内侧的延迟函数结构体
m *m // 当前 Goroutine 占用的线程,可能为空
sched gobuf // 存储 Goroutine 的调度相关的数据
atomicstatus uint32 // Goroutine 的状态
goid int64 // Goroutine 的 ID
}
- 等待中
- 可运行
- 运行中
M
最多只会有 GOMAXPROCS 个活跃线程能够正常运行
type m struct {
g0 *g // 持有调度栈的特殊Goroutine, 深度参与运行时的调度过程
curg *g // 当前线程上运行的用户 Goroutine
p puintptr // 运行代码的处理器
nextp puintptr // 暂存的处理器
oldp puintptr // 执行系统调用之前使用线程的处理器
...
}
P
线程和 Goroutine 的中间层, 提供线程需要的上下文环境, 也负责调度线程上等待执行的Goroutine队列.
P 的数量等于 GOMAXPROCS 的数量
调度器启动
创建M 和 P
创建 Goroutine
- 初始化 Goroutine 结构体
- 运行队列
- 本地P队列
- 调度器全局队列
- 调度信息
调度
- 为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过 schedtick 保证有一定几率会从全局的运行队列中查找对应的 Goroutine
- 从处理器本地的运行队列中查找待执行的 Goroutine
- 阻塞地从本地运行队列、全局运行队列中查找
- 从网络轮询器中查找是否有 Goroutine 等待运行
- 通过 runtime.runqsteal 尝试从其他随机的处理器中窃取待运行的 Goroutine,该函数还可能窃取处理器的计时器
- 获取Goroutine 并执行后, 再次出发新一轮的Goroutine调度
触发调度的时机:
- Goroutine主动挂起
- 系统调用
- Goroutine执行结束退出
- 协作式调度
- 信号抢占式调度
网络轮训器(netpoll)
运行时用来处理 I/O 操作的关键组件
网络轮询器并不是由运行时中的某一个线程独立运行的,运行时的调度器和系统调用都会通过 runtime.netpoll 与网络轮询器交换消息,获取待执行的 Goroutine 列表,并将待执行的 Goroutine 加入运行队列等待处理。
系统监控(sysmon)
它在内部启动了一个不会中止的循环,在循环的内部会轮询网络、抢占长期运行或者处于系统调用的 Goroutine 以及触发垃圾回收
系统监控在创建的线程(M)上执行, 不需要处理器§
- 检查死锁
- 运行计时器 — 获取下一个需要被触发的计时器
- 轮询网络 — 获取需要处理的到期文件描述符
- 抢占处理器 — 抢占运行时间较长的或者处于系统调用的 Goroutine
- 垃圾回收 — 在满足条件时触发垃圾收集回收内存