go设计与实现学习笔记
map
主要结构
-
hash种子,hash函数,bucket对数B,bmap数组,溢出桶,每个桶最多存储8个键值对
溢出桶
-
当单个bucket元素数量超过8,会向溢出桶存储数据
- 桶数量<2^4,不创建,使用几率小
- 桶数量>24,创建2(B-4)个溢出桶
-
冲突解决:链地址法
访问
- 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
作用
- 协调多个gorountine的执行顺序
主要结构
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
- noCopy在编译期间确保不会被拷贝
- state1存储状态以及信号量,这个操作很骚,反正就是32和64位机器存储不太一样,主要包括sema信号量,counter计数器,waiter计数器
Add
- CAS将counter++
Done
- CAS将counter--
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
作用
- 在goroutine之间并发安全的传输数据
主要结构
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 为空时;
- 当缓冲区中不存在数据并且也不存在数据的发送者时;