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 的接收操作处理到的位置;
  • sendqrecvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,这些等待队列使用双向链表 runtime.waitq 表示,链表中所有的元素都是 runtime.sudog 结构

发送数据

  1. 如果当前 Channel 的 recvq 上存在已经被阻塞的 Goroutine,那么会直接将数据发送给当前 Goroutine 并将其设置成下一个运行的 Goroutine;
  2. 如果 Channel 存在缓冲区并且其中还有空闲的容量,我们会直接将数据存储到缓冲区 sendx 所在的位置上;
  3. 如果不满足上面的两种情况,会创建一个 runtime.sudog 结构并将其加入 Channel 的 sendq 队列中,当前 Goroutine 也会陷入阻塞等待其他的协程从 Channel 接收数据;

发送数据的过程中包含几个会触发 Goroutine 调度的时机:

  1. 发送数据时发现 Channel 上存在等待接收数据的 Goroutine,立刻设置处理器的 runnext 属性,但是并不会立刻触发调度;
  2. 发送数据时并没有找到接收方并且缓冲区已经满了,这时会将自己加入 Channel 的 sendq 队列并调用 runtime.goparkunlock 触发 Goroutine 的调度让出处理器的使用权;

接收数据

  1. 如果 Channel 为空,那么会直接调用 runtime.gopark 挂起当前 Goroutine;
  2. 如果 Channel 已经关闭并且缓冲区没有任何数据,runtime.chanrecv 会直接返回;
  3. 如果 Channel 的 sendq 队列中存在挂起的 Goroutine,会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区;
  4. 如果 Channel 的缓冲区中包含数据,那么直接读取 recvx 索引对应的数据;
  5. 在默认情况下会挂起当前的 Goroutine,将 runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒;

我们总结一下从 Channel 接收数据时,会触发 Goroutine 调度的两个时机:

  1. 当 Channel 为空时;
  2. 当缓冲区中不存在数据并且也不存在数据的发送者时;
posted @ 2021-10-18 19:36  朕蹲厕唱忐忑  阅读(76)  评论(0编辑  收藏  举报