容器

Map实现

1,结构体,map是个指针,底层指向hmap,所以是个引用类型

image-20220817124751333

2,定位

===>计算hash(key),每种类型的哈希方法由系统初始化,再加上随机种子hash := alg.hash(key, uintptr(h.hash0))

===>定位桶:根据hash值(64位机下共 64 个 bit 位)的后B位定位桶,b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&mask)*uintptr(t.bucketsize)))。如果map未在扩容或者当前bucket已经完成扩容,在当前bucket中查找,若当前bucket正在扩容,则到老bucket查找。

===>定位槽:每个key生成了一个tophash,取值为hash值前8位。先和tophash的每个值比较,若不等,则key一定不在该槽,若相等,进一步调用alg.equal(key, k)方法验证。

image-20220817125258218

===>当前桶遍历完没有key时,若bmap.overflow不为空,需要继续寻找溢出桶,定位方式和之前一样。

3,扩容

触发:元素个数大于8且元素个数大于 6.5 * 桶的数量;溢出桶过多(此时哈希表中可能元素不多,但有大量桶,且有大量空槽,这种场景出现的原因一般是先大量添加元素,再大量删除,再插入很多元素,就会留下一些空洞,定位key需要额外扫描空的位置,降低性能,需要进行处理)。

若为元素过多,则将数组容量翻倍。否则是溢出桶过多,采用原桶容量(元素不多,不需要更大的容量,只是需要重新整合,消除溢出桶)

渐进式扩容,在插入、修改或者删除时,使用(oldbuckets != nil )触发搬迁数据,每次搬迁2个桶到新桶上,其中一个是key所在的桶,和一个另外的桶,新桶下标计算与java相同。这样扩容的好处是将扩容的复杂度均摊到每次操作,保证在map操作上耗时稳定,缺点是实现复杂。

在扩容过程中,写入数据时会把该key对应的老bucket迁移,并将数据写入新桶;遍历时key对应的老桶还没被搬迁,则需要到老桶上找元素。

4,遍历

随机性:每次for-range遍历map的顺序不一样。开始遍历的桶startBucket不一样,且遍历每个桶时开始位置offset也不同(不同rang的offset不同,同一个range内全部桶得offset相同)。编程语言没有定义map的迭代顺序,不同平台的遍历顺序可能不一样,这导致基于特定顺序的代码在不同平台之间的运行结果可能不一致,另外map扩容后,一个bucket的数据会分散到两个bucket,也会导致顺序产生变化,因此为了防止程序员依赖特定的迭代顺序,map的迭代就不可预测。如果想顺序遍历 map,需要对 map key 先排序,再按照 key 的顺序遍历 map。

考虑扩容:由于是渐进式扩容,可能遍历的过程中同时扩容也在进行,有些bucket可能已经被搬到新map,有些没有。因此遍历时需要考虑在新老哪个map取数据。

map无法并发读写,需要对写和读操作进行加锁,或者使用使用sync.Map。

sync.Map

虽然read和dirty有冗余数据,但这些数据是通过指针指向同一个数据,所以尽管Map的value会很大,但是冗余的空间占用还是有限的。read的数据结构是:

readOnly.m和Map.dirty存储的值类型是*entry,它包含一个指针p, 指向用户存储的value值。

1,加载方法,也就是提供一个键key,查找对应的值value,如果不存在,通过ok反映:

如果我们查询的键值正好存在于m.read中,无须加锁,直接返回,理论上性能优异。即使不存在于m.read中,经过miss几次之后,m.dirty会被提升为m.read,又会从m.read中查找。所以对于更新/增加较少,加载存在的key很多的case,性能基本和无锁的map类似。

2,missLocked方法中可能会将m.dirty提升。

切片实现

1,使用指针指向底层数组,作为参数传递时指针作为值被复制,由同一个数组生成的两个切片是两个不同的变量,只有通过array指针指向同一个底层数组。当切片作为参数时,其实也是切片的拷贝,在拷贝的切片中其包含的指针成员变量的值是一样的,也就是说它们指向的数据源是一样

2,初始化

3,扩容,扩容切片指向新开辟的数组,而对于上层切片来说是没有变化的。

slice是并发不安全的。slice在并发执行中不会报错,但是数据会丢失,可以在写入数据时加锁,保证同一时间只能有一个在执行写操作。或者将要写入的数据写入channel,由另一个独立的协程负责向切片写入数据。

channel

用于在协程间传递消息,收发遵循先进先出 FIFO,golang实现基于通信的资源内存,可实现生产者、消费者模型,以及多协程间的并发控制(缓存区大小即为资源数,占用资源时就可以向channel写入,结束资源占用就从channel读出,缓冲区满其它写入协程将阻塞)。

  1. 给⼀个 nil channel 发送、接收数据,造成永远阻塞
  2. 给⼀个已经关闭的 channel 发送数据,引起 panic
  3. 从⼀个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回⼀个零值,非空返回缓冲区值
  4. ⽆缓冲的 channel 是同步的,⽽有缓冲的 channel 是⾮同步的
  5. 关闭⼀个 nil channel 将会发⽣ panic

channel 中使⽤了 ring buffer(环形缓冲区)来缓存写⼊的数据。

image-20220817164544863

  1. 若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。
  2. 若等待接收队列 recvq 空,缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
  3. 若等待接收队列 recvq 空,缓冲区中没有空余位置,则将要发送的数据和当前的Groutine打包成Sudog对象放入sendq,并将groutine置为等待状态,等待被读 goroutine 唤醒。
  1. 若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。
  2. 如果等待发送队列 sendq 不为空,且缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。
  3. 若等待发送队列 sendq 空,且缓冲区中有数据,则从缓冲区取出数据,结束读取过程。
  4. 若等待发送队列 sendq 空,且缓冲区空,则阻塞该Groutine,并将groutine打包为sudogo加入到recevq等待队列中,等待被写 goroutine 唤醒。