企业官网建站,自建网站平台 优帮云,安卓软件商店,WordPress和微信同步在 golang中#xff0c;想要并发安全的操作map#xff0c;可以使用sync.Map结构#xff0c;sync.Map 是一个适合读多写少的数据结构#xff0c;今天我们来看看它的设计思想#xff0c;来看看为什么说它适合读多写少的场景。 如下#xff0c;是golang 中sync.Map的数据结构… 在 golang中想要并发安全的操作map可以使用sync.Map结构sync.Map 是一个适合读多写少的数据结构今天我们来看看它的设计思想来看看为什么说它适合读多写少的场景。 如下是golang 中sync.Map的数据结构其中 属性read 是 只读的 mapdirty 是负责写入的mapsync.Map中的键值对value值本质上都是entry指针类型entry中的p才指向了实际存储的value值。
// sync.Map的核心数据结构
type Map struct {mu Mutex // 对 dirty 加锁保护线程安全read atomic.Value // read 只读的 map充当缓存层dirty map[interface{}]*entry // 负责写操作的 map当misses len(dirty)时将其赋值给readmisses int // 未命中 read 时的累加计数每次1
}
// 上面read字段的数据结构
type readOnly struct {m map[interface{}]*entry // amended bool // Map.dirty的数据和这里read中 m 的数据不一样时为true
}// 上面m字段中的entry类型
type entry struct {// value是个指针类型p unsafe.Pointer // *interface{}
}我们从一个sync.Map的数据写入和数据查询 两个过程来分析这两个map中数据的变化。
我将不展示具体的代码仅仅讲述数据的流动相信懂了这个以后再去看代码应该不难。
步骤一: 首先是一个初始的sync.Map 结构我们往其中写入数据数据会写到dirty中同时由于sync.Map 刚刚创建所以read map还不存在所以这里会先初始化一个read map 。amended 是read map中的一个属性为true代表 dirty 和read中数据不一致。 步骤二: 接着如果后续再继续写入新数据 在read map没有从dirty 同步数据之前即amended 变为false之前再写入新键值对都只会往dirty里写。 步骤三: 如果有读操作sync.Map 都会尽可能的让其先读read mapread map读取不到并且amended 为true即read 和dirty 数据不一致时会去读dirty读dirty的过程是上锁的。 步骤四: 当读取read map中miss次数大于等于dirty数组的长度时会触发dirty map整体更新为readOnly map并且这个过程是阻塞的。更新完成后原先dirty会被置为空amended 为false代表read map同步了之前所有的数据。如下图所示 整体更新的逻辑是直接替换变量的值并非挨个复制
func (m *Map) missLocked() {m.missesif m.misses len(m.dirty) {return}// 将dirty置给read因为穿透概率太大了(原子操作耗时很小)m.read.Store(readOnly{m: m.dirty})m.dirty nilm.misses 0
}步骤五: 如果后续sync.Map 不再插入新数据那么读取时就可以一直读取read map中的数据了直接读取read map 中的key是十分高效的只需要用atomic.Load 操作 取到readOnly map结构体然后从中取出特定的key就行。
如果读miss了因为没有插入新数据read.amendedfalse 代表read 是保存了所有的kv键值对读miss后也不会再去读取dirty了也就不会有读dirty加锁的过程。
// 上面read字段的数据结构
type readOnly struct {m map[interface{}]*entry // amended bool // Map.dirty的数据和这里read中 m 的数据不一样时为true
}func (m *Map) Load(key interface{}) (value interface{}, ok bool) {// 因read只读线程安全优先读取read, _ : m.read.Load().(readOnly)e, ok : read.m[key]// 如果read没有并且dirty有新数据那么去dirty中查找read.amendedtruedirty和read数据不一致// 暂时省略 后续代码.......}上面的获取key对应的value过程甚至比RWMutex 读锁下获取map中的value还要高效毕竟RWmutex 读取时还需要加上读锁其底层是用atomic.AddInt32 操作而sync.Map 则是用 atomic.load 获取mapatomic.AddInt32 的开销比atomic.load 的开销要大。 所以为什么我们说golang的sync.Map 在大量读的情况下性能极佳因为在整个读取过程中没有锁开销atomic.load 原子操作消耗极低。 但是如果后续又写入了新的键值对数据那么 dirty map中就会又插入到新的键值对dirty和read的数据又不一致了read 的amended 将改为true。
并且由于之前dirty整体更新为read后dirty字段置为nil了所以在更改amended时也会将read中的所有未被删除的key同步到 dirty中。 注意为什么在dirty整体更新一次read map后再写入新的键值对时需要将read map中的数据全部同步到dirty因为随着dirty的慢慢写入后续读操作又会造成读miss的增加最终会再次触发dirty map整体更新为readOnly mapamended 改为false代表read map中又有所有键值对数据了也就是会回到步骤三的操作重复步骤三到步骤五的过程。 只有将read map中的数据全部同步到dirty ,才能保证后续的整体更新不会造成丢失数据。
看到这里应该能够明白sync.Map的适合场景了我来总结下
sync.Map 适合读多写少的场景大量的读操作可以通过只读取read map 拥有极好的性能。
而如果写操作增加首先会造成read map中读取miss增加会回源到dirty中读取且dirty可能会频繁整体更新为read回源读取整体更新的步骤都是阻塞上锁的。
其次写操作也会带来dirty和 read中数据频繁的不一致导致read中的数据需要同步到dirty中这个过程在键值对比较多时性能损耗较大且整个过程是阻塞的。
所以sync.Map 并不适合大量写操作。