GO之切片与MAP的线程安全原理

卷心菜

2021-07-03 16:29
关注

一、slice线程不安全

如下为示例代码,slice运用append进行元素的添加;需要注意的是会存在线程不安全的问题

情况1:线程全的情况主要是在内存的扩容,在底层中对于slice在容量不足的时候会执行扩容的操作,去申请内存;

情况2:存在的线程不安全主要是在存在有空闲的空间的时候可以存放元素的时候另一个协程也关注到这个空间,两个协程同时写入到同一个空间的时候就会出现竞争;最终只有一个写入

解决的方案就是利用sync的锁机制

二、MAP线程不安全

如下为示例代码,map利用协程测试在并发的情况下的操作,需要注意的是map的线程不安全会出现报错的现象

Map在进行查找、赋值、遍历、删除的操作的时候都会对hamp.flags进行标记,如果发现有标记则直接panic;go不对map做线程安全处理主要是因为考虑性能因为有些场景可以不用考虑这个话题

Go这样的设计,主要是在goroutine操作时,可能会因为并发的情况造成混乱,相关的程序也可能会发生不可预知的问题

对于map的解决方案有两种 1. 是运用sync.RWMutex 2. 是运用sync.Map

方案一:运用sync.RWMutex

方案二:运用sync.Map

三、sync.map分析

Go语言元素map并不是线程安全的,而程序中对它进行并发读写操作的时候,需要加锁。而go源码提供了sync.map则是一种并发安全的map

sync.map map 的读写,不需要加锁。通过空间换时间的方式,使用 read dirty 两个 map 来进行读写分离,降低锁时间来提高效率

源码:src/sync/map.go中

Map.read实际是readOnly这个结构体,只读的数据结构体,因为只读所以不会有写冲突,而readOnly包含了map的一部分数据,用于并发安全的访问

Map.dirty数据包含当前的map包含的key,包含最新的key

注意在dirtyreadOnly.m中虽然存在冗余但是它们的value都是指向同一个指针变量*entry

Entry是实际用于存储value的结构体,里面的p实际是一个*interface{},也就是entry实际保存的是指向value的指针;

Sync.Map整体的优化有如下几点

1. 空间换时间。通过冗余的两个数据结构(readdirty),实现加锁对性能的影响。

2.map只保存key和对应的value的指针,这样可以并发的读写map, 实际更新指向

3. value的指针再通过基于CAS的无锁atomic

4. 使用只读数据(read),避免读写冲突

5. 动态调整,miss次数多了之后,将dirty数据提升为read

6. 延迟删除。删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。

7. 优先从read读取、更新、删除,因为对read的读取不需要锁。

举报/反馈