主要结构
访问
- key+hash得到hash,hash&(2^B-1)得到bucket序号,遍历bucket(包括溢出桶),topHash可以加速这个过程
写入
扩容
- 时机:装载因子超过6.5,或者使用了太多溢出桶
- 等量扩容,map数据全部删除,但是数据未超过阈值,会积累造成缓慢内存泄露
- 翻倍扩容:oldbuckets 存原来,buckets存新的双倍,lowHash就可以确定分流到哪个bucket,避免性能巨大抖动。每次写入数据时,会触发对应单元分流,并更新计数器。当计数完成,删除oldbuckets。
sync.Mutex
作用
主要结构
- state uint32,互斥所状态,29位表示等待goroutine个数,1位标识是否被锁定,1位标识是否饥饿,1位标识是否从正常模式被唤醒
- sema,锁信号量
饥饿模式
- 一般会先进先出的形式获取锁,为了避免刚唤起的g与刚创建的g竞争时获取不到锁,设置了此模式。此模式下,会直接将锁交给等待队列最前的g,防止高尾延时。
正常模式
- 当g获取锁,切在队列末尾,或者等待时间少于1ms,恢复正常模式
加锁
- 锁状态非0,已经被占用,开始自旋等待,执行PAUSE,占用CPU
- 普通模式
- 运行在多cpu机器
- 当前g为获取此锁自旋次数<4
- 机器至少存在一个p正在运行,且g队列为空
- 以上条件均满足,才会进入自旋
- 自旋成功,锁状态变成0,根据上下文计算state最新字段
- 通过CAS操作更新state,尝试获取锁,获取成功
- 正常模式,设置唤醒和饥饿标记,重置迭代次数
- 饥饿模式,判断是否可以进入正常模式
- 获取失败,阻塞,等待持有者唤醒
解锁
- CAS操作解锁
- 新状态==0,成功
- 新状态!=0,慢速解锁
- 锁是否合法,是否已经被其他人释放,报错
- 正常模式,如果不存在等待者,或者lock 饥饿 唤醒标志位均不为0,直接返回;如果存在等待者,唤醒并移交所有权
- 饥饿模式,将当前锁交给下一个等待者,同时维持饥饿模式
sync.RWMutex
作用
主要结构
type RWMutex struct {
w Mutex
// 读写的信号量
writerSem uint32
readerSem uint32
// 执行的读个数
readerCount int32
// 读被写阻塞的个数
readerWait int32
}
获取写锁
- 获取mutex,阻塞后续写操作
- CAS操作,将readerCount减少到负数,阻塞后续的读操作
- 如果有其他G持有读锁,即readerWait>0,当前线程进入休眠状态,等待所有读锁拥有者执行结束,释放writerSem,唤醒当前线程
- 注意:先阻塞写操作,后阻塞读操作,能保证读操作不会被连续的写操作饿死
释放写锁
- 基本流程与获取写锁相反
- CAS将readerCount归回正数,释放读锁
- for循环释放所有因为获取读锁而阻塞的G
- 释放mutex,释放写锁
获取读锁
- CAS将readerCount++
- 返回负数,说明其他G获取的写锁,当前G阻塞,等待锁释放
- 返回非负数,成功加读锁
释放读锁
- CAS将readerCount--
- 返回非负数,解锁成功
- 返回负数,有写操作正在执行,但是被阻塞了,开始慢解锁
- CAS将readerWait--
- 如果减至0,说明没有G持有读锁,唤醒写操作的G
sync.WaitGroup
作用
主要结构
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
- noCopy在编译期间确保不会被拷贝
- state1存储状态以及信号量,这个操作很骚,反正就是32和64位机器存储不太一样,主要包括sema信号量,counter计数器,waiter计数器
Add
Done
Wait
- 如果counter计数器大于0,且不存在等待的G,陷入休眠
- 如果counter计数器归零,唤醒睡眠的G
sync.Once
作用
主要结构
type Once struct {
done uint32
m Mutex
}
Do
-
函数已经执行过,即done==1,直接返回
-
函数未执行过,即done==0,开始doSlow
- mutex加锁
- defer mutex 解锁
- 如果done==0
- defer CAS done++(防止函数panic)
- 执行函数
channel
作用
主要结构
type hchan struct {
qcount uint
dataqsiz uint
buf unsafe.Pointer
elemsize uint16
closed uint32
elemtype *_type
sendx uint
recvx uint
recvq waitq
sendq waitq
lock mutex
}
-
qcount — Channel 中的元素个数;
-
dataqsiz — Channel 中的循环队列的长度;
-
buf — Channel 的缓冲区数据指针;
-
sendx — Channel 的发送操作处理到的位置;
-
recvx — Channel 的接收操作处理到的位置;
-
sendq 和 recvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,这些等待队列使用双向链表 runtime.waitq 表示,链表中所有的元素都是 runtime.sudog 结构
发送数据
- 如果当前 Channel 的
recvq 上存在已经被阻塞的 Goroutine,那么会直接将数据发送给当前 Goroutine 并将其设置成下一个运行的 Goroutine;
- 如果 Channel 存在缓冲区并且其中还有空闲的容量,我们会直接将数据存储到缓冲区
sendx 所在的位置上;
- 如果不满足上面的两种情况,会创建一个
runtime.sudog 结构并将其加入 Channel 的 sendq 队列中,当前 Goroutine 也会陷入阻塞等待其他的协程从 Channel 接收数据;
发送数据的过程中包含几个会触发 Goroutine 调度的时机:
- 发送数据时发现 Channel 上存在等待接收数据的 Goroutine,立刻设置处理器的
runnext 属性,但是并不会立刻触发调度;
- 发送数据时并没有找到接收方并且缓冲区已经满了,这时会将自己加入 Channel 的
sendq 队列并调用 runtime.goparkunlock 触发 Goroutine 的调度让出处理器的使用权;
接收数据
- 如果 Channel 为空,那么会直接调用
runtime.gopark 挂起当前 Goroutine;
- 如果 Channel 已经关闭并且缓冲区没有任何数据,
runtime.chanrecv 会直接返回;
- 如果 Channel 的
sendq 队列中存在挂起的 Goroutine,会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区;
- 如果 Channel 的缓冲区中包含数据,那么直接读取
recvx 索引对应的数据;
- 在默认情况下会挂起当前的 Goroutine,将
runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒;
我们总结一下从 Channel 接收数据时,会触发 Goroutine 调度的两个时机:
- 当 Channel 为空时;
- 当缓冲区中不存在数据并且也不存在数据的发送者时;
|
请发表评论