大家好,今天将梳理出的 Go语言并发知识内容,分享给大家。 请多多指教,谢谢。
本文主要介绍 sync 标准库中的 sync.Map
介绍及使用。
本章节内容
- sync.Map
sync.Map
介绍
sync.Map 类似于 Go内置 Map[interface{}]interface{}
,但对于多个 goroutine 并发使用是安全的,无需额外的锁或协调。加载、存储和删除在平摊常数时间内运行。
sync.Map类型针对两个常见的用例进行了优化:
-
当一个给定键的条目只写入一次而被多次读取时,就像在缓存中只会增长一样。
-
当多个 goroutine 对不相交的键集读、写和覆盖条目时。在这两种情况下,与单独
Mutex
或RWMutex
配对的Go Map相比,使用sync.Map
类型可以显著减少锁争用。
总之来说适合大量读,少量写。
历史版本
在Go v1.6之前,内置 map 是部分 goroutine 安全的,并发读没有问题,并发写可能有问题。
在Go v1.6之后,并发读写内置 map 会报错,在一些知名的开源库都有这个问题,所以在Go v1.9之前,解决方案是加一个额外的大锁,锁住map。
在Go v1.9之后,Go官方推出了 Map
、sync.Map
。
sync.Map 类型原型
// entry 键值对中的值结构体
type entry struct {
p unsafe.Pointer // 指针,指向实际存储value值的地方
}
// Map 并发安全的map结构体
type Map struct {
mu sync.Mutex // 锁,保护read和dirty字段
read atomic.Value // 存仅读数据,原子操作,并发读安全,实际存储readOnly类型的数据
dirty map[interface{}]*entry // 存最新写入的数据
misses int // 计数器,每次在read字段中没找所需数据时,+1
// 当此值到达一定阈值时,将dirty字段赋值给read
}
// readOnly 存储mao中仅读数据的结构体
type readOnly struct {
m map[interface{}]*entry // 其底层依然是个最简单的map
amended bool // 标志位,标识m.dirty中存储的数据是否和m.read中的不一样,flase 相同,true不相同
}
func (m *Map) Delete(key any)
func (m *Map) Load(key any) (value any, ok bool)
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
func (m *Map) Range(f func(key, value any) bool)
func (m *Map) Store(key, value any)
sync.Map
中key和value是分开存放的,key通过内置map指向entry,entry通过指针,指向value实际内存地址。
需要注意的地方:
- read在进行非读操作时,需要锁mu进行保护
- 写入的数据,都是直接写到dirty,后面根据read miss次数达到阈值,会进行read和dirty数据的同步
- readOnly中专门有一个标志位,用来标注read和dirty中是否有不同,以便进行read和dirty数据同步
func (m *Map) Delete(key any)
方法:删除键的值。
func (m *Map) Load(key any)
方法: 返回存储在映射中的某个键的值,如果没有值,则返回nil。ok结果表明是否在映射中找到了值。
func (m *Map) LoadAndDelete(key any)
方法:删除键的值,如果有则返回前一个值。加载的结果报告键是否存在。
func (m *Map) LoadOrStore(key, value any)
方法:如果存在,则返回键的现有值。否则,它存储并返回给定的值。如果该值已加载,则加载结果为true,如果已存储则为false。
func (m *Map) Range(f func(key, value any) bool)
方法:Range对映射中出现的每个键和值依次调用f。如果f返回false, range将停止迭代。
func (m *Map) Store(key, value any)
方法:设置键的值。
sync.Map 设计思想
空间换时间
sync.Map
中冗余的数据结构就是 dirty
和 read
,二者存放的都是 key-entry
,entry
其实是一个指针,指向 value
,read
和 dirty
各自维护一套 key
,key
指向的都是同一个 value
,也就是说,只要修改了这个entry
,对 read
和 dirty
都是可见的。
拿空间换时间策略在 sync.Map
中的体现:
- 遍历操作:只需遍历read即可,而read是并发读安全的,没有锁,相比于加锁方案,性能大为提升
- 查找操作:先在read中查找,read中找不到再去dirty中找
核心思想就是一切操作先去read中执行,因为read是并发读安全的,无需锁,实在在read中找不到,再去dirty中。read在sycn.Map
中是一种冗余的数据结构,因为read和dirty中数据有很大一部分是重复的,而且二者还会进行数据同步。
读写分离
sync.Map
中有专门用于读的数据结构:read,将其和写操作分离开来,可以避免读写冲突。而采用读写分离策略的代价就是冗余的数据结构,其实还是空间换时间的思想。
双检查机制
在 sync.Map
中,每次当 read 不符合要求要去操作 dirty 前, 都会上锁, 上锁后再次判断是否符合要求, 因为 read 有可能在上锁期间,产生了变化,突然又符合要求了。
通过额外的一次检查操作,来避免在第一次检查操作完成后,其他的操作使得检查条件产生突然符合要求的可能。
延迟删除
在删除操作中,删除 key-value 仅仅只是先将需要删除的 key-value 打一个标记,这样可以尽快的让 delete 操作先返回,减少耗时,在后面提升 dirty 时,在一次性的删除需要删除的 key-value。
read 优先
需要进行读取、删除、更新操作时,优先操作 read,因为 read 无锁的,更快。 如果在 read 中得不到结果,再去 dirty中。
read 的修改操作需要加锁, read只是并发读安全,并发写并不安全。
状态机制
entry 的指针是有状态的,主要分为:nil、expunged(指向被删除的元素)、正常状态。
主要是两个操作会引起 entry指针状态的变化:Store()
(新增/修改)和 Delete()
(删除)
sync.Map 实践
基本使用
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 写入
m.Store("key1", 1)
m.Store("key2", 2)
// 读取
value, _ := m.Load("key1")
fmt.Println(value.(int))
// 遍历
m.Range(func(key, value interface{}) bool {
k := key.(string)
v := value.(int)
fmt.Println(k, v)
return true
})
// 删除
m.Delete("key1")
value, ok := m.Load("key1")
fmt.Println(value, ok)
// 读取或写入
m.LoadOrStore("key2", 22)
value, _ = m.Load("key2")
fmt.Println(value)
}
输出
1
key1 1
key2 2
<nil> false
2
第 1 步,写入两个 k-v 对;
第 2 步,使用 Load 方法读取其中的一个 key;
第 3 步,遍历所有的 k-v 对,并打印出来;
第 4 步,删除其中的一个 key,再读这个 key,得到的就是 nil;
第 5 步,使用 LoadOrStore,尝试读取或写入 “key2”,因为这个 key 已经存在,因此写入不成功,并且读出原值。
解决并发中write问题
golang的map是非协程安全的,并发写是会出现错误
package main
func main() {
m := map[int]int{1:1}
go concurrent(m)
go concurrent(m)
select{}
}
func concurrent(m map[int]int) {
i := 0
for i < 10000 {
m[1] = 1 // 频繁写
i++
}
}
输出
fatal error: concurrent map writes
在多个goroutine中,map不能同时写。
使用 sync.Map
解决不能同时写问题
package main
import (
"fmt"
"sync"
"time"
)
func main() {
m := sync.Map{}
m.Store(1, 1)
go concurrent(m)
go concurrent(m)
time.Sleep(3*time.Second)
fmt.Println(m.Load(1))
}
func concurrent(m sync.Map) {
i := 0
for i < 10000 {
m.Store(1, 1) // 频繁写
i++
}
}
输出
1 true
互斥锁与sync.Map效率对比
package main
import (
"fmt"
"time"
"sync"
)
var s sync.RWMutex
var w sync.WaitGroup
func main() {
mapTest()
syncMapTest()
}
func mapTest() {
m := map[int]int {1:1}
startTime := time.Now().Nanosecond()
w.Add(1)
go writeMap(m)
w.Add(1)
go writeMap(m)
w.Add(1)
go readMap(m)
w.Wait()
endTime := time.Now().Nanosecond()
timeDiff := endTime-startTime
fmt.Println("map:",timeDiff)
}
func writeMap (m map[int]int) {
defer w.Done()
i := 0
for i < 10000 {
// 加锁
s.Lock()
m[1]=1
// 解锁
s.Unlock()
i++
}
}
func readMap (m map[int]int) {
defer w.Done()
i := 0
for i < 10000 {
s.RLock()
_ = m[1]
s.RUnlock()
i++
}
}
func syncMapTest() {
m := sync.Map{}
m.Store(1,1)
startTime := time.Now().Nanosecond()
w.Add(1)
go writeSyncMap(m)
w.Add(1)
go writeSyncMap(m)
w.Add(1)
go readSyncMap(m)
w.Wait()
endTime := time.Now().Nanosecond()
timeDiff := endTime-startTime
fmt.Println("sync.Map:",timeDiff)
}
func writeSyncMap (m sync.Map) {
defer w.Done()
i := 0
for i < 10000 {
m.Store(1,1)
i++
}
}
func readSyncMap (m sync.Map) {
defer w.Done()
i := 0
for i < 10000 {
m.Load(1)
i++
}
}
对比结果如下:
情况 | 结果 |
---|---|
只写 | map: 1374000 sync.Map: 2899000 |
读写 | map: 7607000 sync.Map: 22295000 |
总结:在大量写的场景下, sync.Map
的效率没有单纯 map + Mutex的效率高。 读写场景下因为互斥锁大量消耗解锁加锁,性能消耗 sync.Map
最优。
技术文章持续更新,请大家多多关注呀~~
搜索微信公众号,关注我【 帽儿山的枪手 】
参考材料
- sync.Map 设计思想和底层源码分析
- 由浅入深聊 Golang的 sync.Map