sync.map

前言

Go中的map不是并发安全的,在Go1.9之后,引入了sync.Map,并发安全的map。

深入了解下

对于map,我们常用的做法就是加锁。

对于sync.Map这些操作则是不需要的,来看下sync.Map的特点:

  • 1、以空间换效率,通过read和dirty两个map来提高读取效率
  • 2、优先从read map中读取(无锁),否则再从dirty map中读取(加锁)
  • 3、动态调整,当misses次数过多时,将dirty map提升为read map
  • 4、延迟删除,删除只是为value打一个标记,在dirty map提升时才执行真正的删除

简单的使用栗子:

func syncMapDemo() {

	var smp sync.Map

	// 数据写入
smp.Store("name", "小红")
smp.Store("age", 18) // 数据读取
name, _ := smp.Load("name")
fmt.Println(name) age, _ := smp.Load("age")
fmt.Println(age) // 遍历
smp.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true
}) // 删除
smp.Delete("age")
age, ok := smp.Load("age")
fmt.Println("删除后的查询")
fmt.Println(age, ok) // 读取或写入,存在就读取,不存在就写入
smp.LoadOrStore("age", 100)
age, _ = smp.Load("age")
fmt.Println("不存在")
fmt.Println(age) smp.LoadOrStore("age", 99)
age, _ = smp.Load("age")
fmt.Println("存在")
fmt.Println(age)
}

查看下具体的实现

// sync/map.go
type Map struct {
// 当写read map 或读写dirty map时 需要上锁
mu Mutex // read map的 k v(entry) 是不变的,删除只是打标记,插入新key会加锁写到dirty中
// 因此对read map的读取无需加锁
read atomic.Value // 保存readOnly结构体 // dirty map 对dirty map的操作需要持有mu锁
dirty map[interface{}]*entry // 当Load操作在read map中未找到,尝试从dirty中进行加载时(不管是否存在),misses+1
// 当misses达到diry map len时,dirty被提升为read 并且重新分配dirty
misses int
} // read map数据结构
type readOnly struct {
m map[interface{}]*entry
// 为true时代表dirty map中含有m中没有的元素
amended bool
} type entry struct {
// 指向实际的interface{}
// p有三种状态:
// p == nil: 键值已经被删除,此时,m.dirty==nil 或 m.dirty[k]指向该entry
// p == expunged: 键值已经被删除, 此时, m.dirty!=nil 且 m.dirty不存在该键值
// 其它情况代表实际interface{}地址 如果m.dirty!=nil 则 m.read[key] 和 m.dirty[key] 指向同一个entry
// 当删除key时,并不实际删除,先CAS entry.p为nil 等到每次dirty map创建时(dirty提升后的第一次新建Key),会将entry.p由nil CAS为expunged
p unsafe.Pointer // *interface{}
}

read mapdirty map 的存储方式是不一致的。

前者使用 atomic.Value,后者只是单纯的使用 map。原因是 read map 使用 lock free 操作,必须保证 load/store 的原子性;而 dirty mapload+store 操作是由 lock(就是 mu)来保护的。

1、read和dirty通过entry包装value,这样使得value的变化和map的变化隔离,前者可以用atomic无锁完成

2、Map的read字段结构体定义为readOnly,这只是针对map[interface{}]*entry而言的,entry内的内容以及amended字段都是可以变的

3、大部分情况下,对已有key的删除(entry.p置为nil)和更新可以直接通过修改entry.p来完成

Load

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 首先在通过atomic的原子操作读取内容
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果没在 read 中找到,并且 amended 为 true,即 dirty 中存在 read 中没有的 key
if !ok && read.amended {
// read调用了atomic的原子性,所以不用加锁,但是dirty map[interface{}]*entry就需要了,用的互斥锁
m.mu.Lock()
// double check,避免在加锁的时候dirty map提升为read map
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 还是没有找到
if !ok && read.amended {
// 从 dirty 中找
e, ok = m.dirty[key]
// 不管dirty中有没有找到 都增加misses计数 该函数可能将dirty map提升为readmap
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
} // 从entry中atomic load实际interface{}
func (e *entry) load() (value interface{}, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
return *(*interface{})(p), true
}

梳理下处理的逻辑:

1、首先是 fast path,直接在 read 中找,如果找到了直接调用 entry 的 load 方法,取出其中的值。

2、如果 read 中没有这个 key,且 amended 为 fase,说明 dirty 为空,那直接返回 空和 false。

3、如果 read 中没有这个 key,且 amended 为 true,说明 dirty 中可能存在我们要找的 key。当然要先上锁,再尝试去 dirty 中查找。在这之前,仍然有一个 double check 的操作。若还是没有在 read 中找到,那么就从 dirty 中找。不管 dirty 中有没有找到,都要"记一笔",因为在 dirty 被提升为 read 之前,都会进入这条路径

// 增加misses计数,并在必要的时候提升dirty map
// 如果 misses 值小于 m.dirty 的长度,就直接返回。否则,将 m.dirty 晋升为 read,并清空 dirty,清空 misses 计数值。这样,之前
// 一段时间新加入的 key 都会进入到 read 中,从而能够提升 read 的命中率。
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
// 提升过程很简单,直接将m.dirty赋给m.read.m
// 提升完成之后 amended == false m.dirty == nil
// m.dirty并不立即创建被拷贝元素,而是延迟创建
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}

对于missLocked会直接将 misses 的值加 1,表示一次未命中,如果 misses 值小于 m.dirty 的长度,就直接返回。否则,将 m.dirty 晋升为 read,并清空 dirty,清空 misses 计数值。这样,之前一段时间新加入的 key 都会进入到 read 中,从而能够提升 read 的命中率。

Store

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
// 如果read map中存在该key 则尝试直接更改(由于修改的是entry内部的pointer,因此dirty map也可见)
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
} m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// 如果 read map 中存在该 key,但 p == expunged,则说明 m.dirty != nil 并且 m.dirty 中不存在该 key 值 此时:
// a. 将 p 的状态由 expunged 更改为 nil
// b. dirty map 插入 key
m.dirty[key] = e
}
// 更新 entry.p = value (read map 和 dirty map 指向同一个 entry)
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// 如果 read map 中不存在该 key,但 dirty map 中存在该 key,直接写入更新 entry(read map 中仍然没有这个 key)
e.storeLocked(&value)
} else {
// 如果read map和dirty map中都不存在该key,则:
// a. 如果dirty map为空,则需要创建dirty map,并从read map中拷贝未删除的元素
// b. 更新amended字段,标识dirty map中存在read map中没有的key
// c. 将k v写入dirty map中,read.m不变
if !read.amended { m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
} // 尝试直接更新entry 如果p == expunged 返回false
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
for {
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
} func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
} // 如果 dirty map为nil,则从read map中拷贝元素到dirty map
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
} read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
// a. 将所有为 nil的 p 置为 expunged
// b. 只拷贝不为expunged 的 p
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
} func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}

梳理下流程:

1、首先还是去read map中查询,存在并且p!=expunged,直接修改。(由于修改的是 entry 内部的 pointer,因此 dirty map 也可见)

2、如果read map中存在该key,但p == expunged。加锁更新p的状态,然后直接更新该entry (此时m.dirty==nilm.dirty[key]==e)

3、如果read map中不存在该Key,但dirty map中存在该key,直接写入更新entry(read map中仍然没有)

4、如果read map和dirty map都不存在该key

  • a. 如果dirty map为空,则需要创建dirty map,并从read map中拷贝未删除的元素
  • b. 更新amended字段,标识dirty map中存在read map中没有的key
  • c. 将k v写入dirty map中,read.m不变

Delete

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
// 从read map中寻找
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 没找到
if !ok && read.amended { // read.amended为true代表dirty map中含有m中没有的元素
m.mu.Lock()
// double check
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 第二次仍然没找到,但dirty map中存在,则直接从dirty map删除
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
// 如果read存在,将entry.p 置为 nil
if ok {
e.delete()
}
} func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}

梳理下流程:

1、先去read map中寻找,如果存在就直接删除

2、如果没找到,并且 read.amended为true代表dirty map中存在,依照传统进行 double check。

3、read map找到就删除,没找到判断dirty map是否存在,存在了就删除

LoadOrStore

func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
// 首先还是先去read map中查询
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
actual, loaded, ok := e.tryLoadOrStore(value)
if ok {
return actual, loaded
}
} m.mu.Lock()
// double check
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// 如果 read map 中存在该 key,但 p == expunged,则说明 m.dirty != nil 并且 m.dirty 中不存在该 key 值 此时:
// a. 将 p 的状态由 expunged 更改为 nil
// b. dirty map 插入 key
m.dirty[key] = e
}
actual, loaded, _ = e.tryLoadOrStore(value)
} else if e, ok := m.dirty[key]; ok {
// 如果 read map 中不存在该 key,但 dirty map 中存在该 key
actual, loaded, _ = e.tryLoadOrStore(value)
// 不管dirty中有没有找到 都增加misses计数 该函数可能将dirty map提升为readmap
m.missLocked()
} else {
if !read.amended {
// 如果read map和dirty map中都不存在该key,则:
// a. 如果dirty map为空,则需要创建dirty map,并从read map中拷贝未删除的元素
// b. 更新amended字段,标识dirty map中存在read map中没有的key
// c. 将k v写入dirty map中,read.m不变
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
actual, loaded = value, false
}
m.mu.Unlock() return actual, loaded
} // 如果entry is expunged则不处理,返回false
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
} // Copy the interface after the first load to make this method more amenable
// to escape analysis: if we hit the "load" path or the entry is expunged, we
// shouldn't bother heap-allocating.
ic := i
for {
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
}
}

这个函数结合了 Load 和 Store 的功能,如果 map 中存在这个 key,那么返回这个 key 对应的 value;否则,将 key-value 存入 map。

这在需要先执行 Load 查看某个 key 是否存在,之后再更新此 key 对应的 value 时很有效,因为 LoadOrStore 可以并发执行。

总结

除了Load/Store/Delete之外,sync.Map还提供了LoadOrStore/Range操作,但没有提供Len()方法,这是因为要统计有效的键值对只能先提

升dirty map(dirty map中可能有read map中没有的键值对),再遍历m.read(由于延迟删除,不是所有的键值对都有效),这其实就是Range做的事情,

因此在不添加新数据结构支持的情况下,sync.Map的长度获取和Range操作是同一复杂度的。这部分只能看官方后续支持。

1、sync.map 是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。

2、通过读写分离,降低锁时间来提高效率,适用于读多写少的场景。

3、Range 操作需要提供一个函数,参数是 k,v,返回值是一个布尔值:f func(key, value interface{}) bool。

4、调用 Load 或 LoadOrStore 函数时,如果在 read 中没有找到 key,则会将 misses 值原子地增加 1,当 misses 增加到和 dirty 的长度相等时,会将 dirty 提升为 read。以期减少“读 miss”。

5、新写入的 key 会保存到 dirty 中,如果这时 dirty 为 nil,就会先新创建一个 dirty,并将 read 中未被删除的元素拷贝到 dirty。

6、当 dirty 为 nil 的时候,read 就代表 map 所有的数据;当 dirty 不为 nil 的时候,dirty 才代表 map 所有的数据。

流程图片

最后附上一张操作的流程图

参考

【Go sync.Map 实现】https://wudaijun.com/2018/02/go-sync-map-implement/

【深度解密 Go 语言之 sync.map】https://www.cnblogs.com/qcrao-2018/p/12833787.html

【golang源码阅读笔记】https://github.com/boilingfrog/Go-POINT/tree/master/golang

【go中sync.Map源码刨铣】https://boilingfrog.github.io/2022/03/16/go中sync.map源码刨铣/

Go中sync.map使用小结的更多相关文章

  1. STL中的map/multimap小结

    (1)使用map/multimap之前必须包含头文件<map>:#include<map> 并且和所有的关联式容器一样,map/multimap通常以平衡二叉树来完成 (2)n ...

  2. Go 1.9 sync.Map揭秘

    Go 1.9 sync.Map揭秘 目录 [−] 有并发问题的map Go 1.9之前的解决方案 sync.Map Load Store Delete Range sync.Map的性能 其它 在Go ...

  3. 看过这篇剖析,你还不懂 Go sync.Map 吗?

    hi, 大家好,我是 haohongfan. 本篇文章会从使用方式和原码角度剖析 sync.Map.不过不管是日常开发还是开源项目中,好像 sync.Map 并没有得到很好的利用,大家还是习惯使用 M ...

  4. sync.Map(在并发环境中使用的map)

    sync.Map 有以下特性: 需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是 ...

  5. golang 标准库 sync.Map 中 nil 和 expunge 区别

    本文不是 sync.Map 源码详细解读,而是聚焦 entry 的不同状态,特别是 nil 状态和 expunge 状态的区分. entry 是 sync.Map 存放值的结构体,其值有三种,分别为 ...

  6. GO 语言中的 sync Map

    为什么需要 sync map go 语言之所以引入 sync.Map主要是因为GO 语言自带的 map 是线程不安全的.只能保证并发的读,但是不能保证并发的写. 看下面的例子: func main() ...

  7. Golang:sync.Map

    由于map在gorountine 上不是安全的,所以在大量并发读写的时候,会出现错误. 在1.9版的时候golang推出了sync.Map. sync.Map 通过阅读源码我们发现sync.Map是通 ...

  8. go的sync.Map

    sync.Map这个数据结构是线程安全的(基本类型Map结构体在并发读写时会panic严重错误),它填补了Map线程不安全的缺陷,不过最好只在需要的情况下使用.它一般用于并发模型中对同一类map结构体 ...

  9. sync.Map与Concurrent Map

    1. sync.Map 1.1. map并发不安全 go1.6以后map有了并发的安全检查,所以如果在并发环境中读写map就会报错 func unsafeMap() { // 创建一个map对象 m ...

  10. 图解Go里面的sync.Map了解编程语言核心实现源码

    基础筑基 在大多数语言中原始map都不是一个线程安全的数据结构,那如果要在多个线程或者goroutine中对线程进行更改就需要加锁,除了加1个大锁,不同的语言还有不同的优化方式, 像在java和go这 ...

随机推荐

  1. 协同导航定位技术:为GPS定位盲区而生

    导航技术和我们的生活息息相关.行人导航系统是一种为行人提供导航服务的便携式设备,可以适应地下.矿洞等卫星信号拒止的地区,以及大商场等拓扑结构复杂的地区,通常基于MIMU实现,本质上是惯性导航系统的一种 ...

  2. three.js项目引入vue,因代码编写不当导致的严重影响性能的问题,卡顿掉帧严重

    three.js项目引入vue,因代码编写不当导致的严重影响性能的问题,卡顿掉帧严重 问题排查 使用谷歌浏览器的Performance分析页面性能 可以看到vue.js的reactiveGetter方 ...

  3. Java 8 Stream原理解析

    说起 Java 8,我们知道 Java 8 大改动之一就是增加函数式编程,而 Stream API 便是函数编程的主角,Stream API 是一种流式的处理数据风格,也就是将要处理的数据当作流,在管 ...

  4. 上海丨阿里云 Serverless 技术实战营邀你来玩!

    活动简介 本次沙龙深度探讨 "Serverless 在中国企业的落地和开发者实操" 主题,我们特别邀请了来自阿里云 一线技术专家,分享当前 Serverless 趋势和落地实践过程 ...

  5. IDEA插件Material Theme UI 激活

    介绍 "Material Theme UI" 是一款为 IntelliJ IDEA 提供现代化材料设计主题的插件,通过重新设计IDE的外观,为开发人员带来更加美观.富有活力的用户体 ...

  6. GCC 指定运行期动态链接库搜索路径

    链接器 ld 的 -rpath=dir 选项可以指定运行期 so 文件的搜索路径. GCC 的 -Wl,option 选项可以传递选项给链接器 ld. 所以组合起来,可以直接使用 -Wl,-rpath ...

  7. 【TouchGFX】IAR 下实现 touchgfx Caching Bitmaps 通过文件方式获取图像资源

    1.Caching Bitmaps 修改缓存方式 2.修改 blockCopy 方法(注意:忘记返回状态导致发生错误) 3.修改分散文件将位于 ExtFlashSection section 数据重定 ...

  8. 云服务器搭建自己的GitServer!

    云服务器搭建自己的GitServer! 如果你有一台云服务器并想在上面搭建自己的 Git 服务器,你可以使用 Git 自带的 git-shell ,也可以使用像 Gitea.GitLab.Gogs 这 ...

  9. MySQL高可用九种方案

    有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准https://blog.zysicyj.top 首发博客地址 参考视频 MMM 方案(单主) MySQL 高可用方案之 MM ...

  10. Nginx的日志处理

    Nginx的日志处理 背景 之前一直被各种咨询nginx的使用问题. 大部分都是性能, 加模块, 以及一些tcp端口等的问题. 其实这些都还好, 还有一个比较麻烦的问题时日志相关的. nginx的日志 ...