前面的两篇文章《2万字图解map》,《从应用层面细说map》从原理层面和应用层面分别介绍了内建map是如何实现的、如何使用,以及使用过程中需要注意的地方。本篇文章将介绍sync.Map的使用和实现分析,通过图解的方式分析p的状态,为什么有expunged状态的存在。建议结合前两篇文章一起阅读,能够更深入的理解map。
Go内建的map类型不是线程安全的,sync.Map是Go1.9中新增加的一个线程安全的map. sync.Map的添加、查询和删除元素操作的时间复杂度与内建的map是一样的,都是常数级别的。与内建map不同的是 ,sync.Map的零值是一个有效的值,它是一个空的map。需要注意的是,sync.Map并不是用来替换内建的map类型,它只能被应用在一些特殊的场景中。
内建的map不是线程安全的,虽然内建的map+Mutex或map+RWMutex可以保证线程安全,但在下面的两个场景中,使用sync.Map会比使用map+RWMutex性能要好很多。
sync.Mapa支持基本的添加元素、查询元素、遍历、删除元素四种操作,此外还支持LoadOrStore操作。Load方法获取一个键值对的value,如果key不存在,返回的value是nil,如果key存在,返回的value是一个interface类型,需要断言成真正的类型。sycn.Map遍历使用Range方法,需要传入一个函数,函数的入参和出参已经确定了,是func(k,v interface) bool。k和v是遍历到的key和value,在函数内部实现自己的业务逻辑。
package main
import (
"fmt"
"sync"
)
func main() {
// m零值是有效的,可以直接使用
var m sync.Map
// 添加元素到sync.Map
m.Store("name", "mingyong")
m.Store("age", 20)
// 访问sync.Map中元素
v, ok := m.Load("name")
fmt.Println(v, ok)
// 遍历sync.Map
m.Range(func(k, v interface{}) bool {
fmt.Println(k, v)
return true
})
// 删除sync.Map中的元素
m.Delete("name")
}
下面将从数据结构定义,到基本的添加元素(Store方法)、查询元素(Load方法)、遍历元素(Range方法)和删除元素(Delete方法)操作,结合源码来分析sync.Map的实现,以及通过图解的方式分析为什么有expunged状态的出现。「注意,下面分析的代码是Go 1.14 darwin-amd64版本」
sync.Map的结构图如下图所示,可以结合下面的结构体定义一起看,就很容易理解了。
先来看下sync.Map的结构定义,它的定义如下,可以看到它只有4个字段。其中mu锁字段用户保护dirty,因为dirty是一个内建的map,是非线程安全的,read也是一个map,对read的读取不需要加锁处理。misses是一个计数器,记录在从read中读取数据的时候,没有命中的次数,一旦misses值和dirty长度相同之后,会把dirty内容提升为read.
type Map struct {
mu Mutex
// 把read看成一个安全的只读的map.atomic.Value是一个interface类型的结构体,里面实际装的是
// 一个map,对atomic.Value中的元素更新是通过原子操作进行的,
read atomic.Value // readOnly
// dirty需要使用上面的mu加锁才能访问里面的元素,dirty中包含所有在read字段中但未被expunged(删除)
// 的元素以及新加的元素
dirty map[interface{}]*entry
// misses是一个计数器,记录在从read中读取数据的时候,没有命中的次数,一旦misses值和dirty长度
// 一样之后,会把dirty内容提升为read
misses int
}
Map中的read字段是atomic.Value类型,它里面实际存储的结构为readOnly,readOnly结构如下,以原子方式存储在read中
// readOnly是存在Map结构中read字段中的内容,它以原子方式存储
type readOnly struct {
m map[interface{}]*entry
// amended为true表示dirty中包含read中没有的数据,为false表示dirty中的数据在read都
// 存在
amended bool // true if the dirty map contains some key not in m.
}
expunged用来标识map中的key是已经删掉的指针,在sync.Map删除一个key时,并不是立即将其从map中删除,而是将key对应的value标记为nil或者expunged,在以后的处理过程中才有机会真正删除
// expunged用来标识map中的key是已经删掉的指针,在sync.Map删除一个key时,并不是立即将其从
// map中删除,而是将key对应的value标记为nil或者expunged,在以后的处理过程中才有机会真正删除
var expunged = unsafe.Pointer(new(interface{}))
entry是dirty这个map value对应的类型,它代表一个值,entry是对*interface{}做了一个结构体的包装。如果dirty字段非nil,map的read字段和dirty字段会包含相同的非expunged的数据项,所以如果通过read字段更改了这个项的值,从dirty字段中也会读取到这个项的新值,因为它们指向的是同一个地址。
type entry struct {
p unsafe.Pointer // *interface{}
}
Store方法用来保存或更新一个键值对。它既可以是新增元素,也可以是更新元素。如果更新的元素在read中已存在,在下面的两种情况下会直接更新,不会用到锁:
在下面的两种情况下更新会用到锁:
// Store保存或更新一个键值对
func (m *Map) Store(key, value interface{}) {
// 检查key是否在read中存在
read, _ := m.read.Load().(readOnly)
// 如果key在read中,有3种情况:
// 1.p=nil,表示key已删除,并且dirty中不存在数据
// 2.p=expunged,表示key已删除,dirty中存在数据且该key不在dirty中
// 3.p=&entry,表示key存在,指向一个真实的value
// 对情况1和情况3,直接将value的值存在p中,对情况2不存value,继续走后面的逻辑
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
m.mu.Lock()
// 加锁后,继续检查read中是否有key存在
read, _ = m.read.Load().(readOnly)
// key在read中,继续检查key是否已经被删除
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// 如果key已被删除,并且处于expunged状态,说明此key存在read但不在dirty中
// 并且此时dirty非空,需要将此key加入到dirty中,并且更新e.p的值指向value
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// key不在read中但在dirty中,直接更新dirty中e.p的值,指向value
e.storeLocked(&value)
} else {
// 走到这里说明key既不在read中也不在dirty中,肯定是一个新的key.
// 并且dirty中所有的key都在read中
if !read.amended {
// 如果dirty为nil,需要创建dirty对象,并且标记read的amended为true,
// 说明有元素存在于dirty中但不在read中
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
// new一个新entry,将新值加入到dirty对象中
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
// tryStore尝试将value的值存在e.p中
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
// 如果p为expunged,不能直接存储,因为此时的read中所有处于非expunged状态的key都
// 在dirty中,将key加回到read的时候,也需要将其加入到dirty中,此处不处理这种情况
// 直接返回
if p == expunged {
return false
}
// p为nil或指向&entry对象,设置e.p为i的值,即将e.p指向存入的value
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
// unexpungeLocked将e.p从expunged修改为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// storeLocked原子操作,将i存储到e.p中,此处的i不能是expunged值
func (e *entry) storeLocked(i *interface{}) {
atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
func (m *Map) dirtyLocked() {
// 如果dirty字段已经存在,就不需要在创建了
if m.dirty != nil {
return
}
// 获取read字段
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
// 遍历read字段,将里面不是punged状态的键值对复制到dirty中
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
Load方法返回一个key对应的value值。优先从read中开始查询,因为访问read不需要加锁。如果很幸运,可以从read中读取到了key对应的value,就不用加锁,这种情况下性能是最好的。但是,如果请求的key不在read中,并且dirty中存在有元素不在read中,需要进一步查询dirty,对dirty操作需要加锁。所以,读取不在read中的key会因为加锁而导致性能下降。Load操作过程中可能存将dirty提升为read操作,在查询dirty的时候,会执行missLocked操作,该操作会增加misses值,如果misses值等于dirty长度,就会将dirty提升为read,并将dirty置为空。
// Load方法返回key对应的value,第二bool型参数表示key是否在map中存在
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
// 优先从无锁map read中读取
e, ok := read.m[key]
// 如果无锁map中不存在并且dirty中有元素不在read中,进一步从dirty中读取
if !ok && read.amended {
m.mu.Lock()
// 双重检查,进一步查看read中是已有key存在
read, _ = m.read.Load().(readOnly)
// 如果key已存在,直接返回对应的value
e, ok = read.m[key]
// key还是不存在,并且dirty中有数据,不得不检查dirty中是否有
if !ok && read.amended {
// 从dirty中获取数据
e, ok = m.dirty[key]
// 不管key在不在dirty中,命中记录数misses都会加1,当misses大于等于
// dirty中数据元素的个数时,dirty中的数据会被提升到read中,提升之后
// dirty将会被清空,命中记录数misses清零
m.missLocked()
}
m.mu.Unlock()
}
// key在read和dirty中都不存在,返回nil和false
if !ok {
return nil, false
}
// 返回value值,e可能来自read也可能来自dirty,所以value可能是从read获得的,也可能是从
// dirty获得的
return e.load()
}
func (m *Map) missLocked() {
// misses计数+1
m.misses++
// 如果没有达到临界值(dirty的长度),直接返回
if m.misses < len(m.dirty) {
return
}
// 将dirty字段的内容提升为read
m.read.Store(readOnly{m: m.dirty})
// 清空dirty,dirty为map类型,清空方法是直接赋值nil,让GC清理掉里面的内容
m.dirty = nil
// misses计数重置为0
m.misses = 0
}
func (e *entry) load() (value interface{}, ok bool) {
// 原子操作,获取e.p中值
p := atomic.LoadPointer(&e.p)
// p为nil或expunged状态都表示key被删除了,所以直接返回nil,false
if p == nil || p == expunged {
return nil, false
}
// 返回value的值
return *(*interface{})(p), true
}
Range方法对sync.Map进行遍历,需要传入一个func(key, value interface{}) bool类型的函数f, 对遍历到的每个键值对调用f进行处理。如果函数f返回false, 对sync.Map的迭代将会停止。
// Range 方法对sync.Map进行遍历操作,需要传入一个func(key, value interface{}) bool类型的
// 函数f,会对遍历到的键值对调用f进行处理,如果函数f返回false,对sync.Map的迭代将停止。
// Range 方法在遍历的时候会对sync.Map的元素至多访问一次,如果在执行Range操作的时候,有其他协程并发
// 的添加或删除元素,可能会导致有些元素未被遍历到。
// Range 方法是一个O(N)时间复杂度的操作,对于存在元素在dirty不在read的情况,进行了一个优化,将dirty
// 提升为read了,所以下次在进行Range的时候,直接对read进行遍历,不用加锁。
func (m *Map) Range(f func(key, value interface{}) bool) {
// 如果所有的元素都在read中,直接对read进行遍历
read, _ := m.read.Load().(readOnly)
// 确认是否有元素存在dirty中而不在read中
if read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if read.amended {
// 有元素在dirty中,对dirty进行遍历
read = readOnly{m: m.dirty}
// 进行一个优化,将dirty提升为read
m.read.Store(read)
// 将dirty提升为read之后,dirty置为nil
m.dirty = nil
// 计数器清理0
m.misses = 0
}
m.mu.Unlock()
}
// 对遍历到的每个元素,调用传入的函数f进行处理
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
Delete方法删除一个元素,同样还是优先检查key是否在read中,如果在read中,就不需要检查dirty了。为啥呢?key在read中分为两种情况,一种是此key只在read中,不在dirty中,很好反正不在dirty中,直接将read中元素删掉就行了,注意此处的删除并不是真正删除,而是标记为一个删除的状态,方便后面又有操作加入此key时,直接修改read中e.p的值。另一种是此key也存在dirty中,此时dirty中key对应的e和read中该key对应的e是同一个,为什么是同一个后面在介绍e.p的状态时有详细说明,这里只需明白它们是同一个e,这种情况将e.p设置为nil,其实也是将dirty中e.p也设置为了nil. 如果key在read中不存在,恰好当前存在元素在dirty中而不在read中,则需要进一步确认key是否在dirty中,这种情况需要加锁,如果key在dirty中,直接调用delete将dirty中的key删除。重复调用Delete操作删除同一个key也是可以的,只有第一次会标记删除,后面调用不做处理,因为此key对应的value已标记为删除状态了。
// Delete删除一个key
func (m *Map) Delete(key interface{}) {
// 先检查key是否在read中
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 如果key不在read中,并且此时dirty中存在数据不在read中,继续检查dirty
if !ok && read.amended {
m.mu.Lock()
// 加锁再进行一次检查
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
// 直接从dirty中删除key,此时read中是不存在的,dirty中删掉之后,两边都是
// 不存在了
delete(m.dirty, key)
}
m.mu.Unlock()
}
// 满足ok为true,read中肯定是有该key的, dirty有两种情况:
// 1是dirty中没有该key, 2是dirty中也有该key
// 对于情况1,因为dirty中不存,直接将read中e.p设置为nil,标记为删除状态
// 对于情况2,dirty中key对应的e和read中该key对应的e是同一个,所以直接将
// read中的e.p设置为nil,其实也是将dirty中e.p也设置为nil了
if ok {
e.delete()
}
}
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
// p为nil和expunged都表示之前已删除过了,直接返回false表示未进行实际的删除
if p == nil || p == expunged {
return false
}
// 将e.p置为nil, key并未从read中删除,如果key存在于dirty中,key也是未从dirty
// 中删除的
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
LoadOrStore方法可以看做Load操作和Store操作的组合,如果key已经在sync.Map中,返回当前key对应的value,否则将存储传入的value值。
// LoadOrStore 可以看做Load操作和Store操作的组合,如果key已存在m中(无论是在read中还是dirty中),
// 就是只要key没有被删除,就返回当前的key对应的value值,否则将存储传入的value值,
// 第二返回参数是一个bool值,表示最后执行的是load操作还是store操作
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
// 还是优先检查read,避免加锁
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
// 尝试load和store操作,如果ok为true,表示load成功
actual, loaded, ok := e.tryLoadOrStore(value)
if ok {
return actual, loaded
}
}
m.mu.Lock()
// 双重检查
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
// e.p为expunged状态,表示m.dirty非空且dirty不存在该key
// 需要将key-value加到dirty中,这里dirty和read实际上key
// 指向的是同一个e,更新e值,dirty和read中都存有该key-value了
if e.unexpungeLocked() {
m.dirty[key] = e
}
actual, loaded, _ = e.tryLoadOrStore(value)
} else if e, ok := m.dirty[key]; ok {
// key在dirty中不在read中,
actual, loaded, _ = e.tryLoadOrStore(value)
m.missLocked()
} else {
if !read.amended {
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
}
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
p := atomic.LoadPointer(&e.p)
// key已被删除,需要进一步判断处理,这里先终止处理
if p == expunged {
return nil, false, false
}
// key存在,value值有效,直接返回之前的value值,即执行Load操作
if p != nil {
return *(*interface{})(p), true, true
}
// 走到这里说明key也是已经被删除,e.p为nil,并且dirty是空的,所以直接将i存储在e.p中即可,不用关心dirty
ic := i
for {
// 原子更新e.p的值,更新前为nil
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
// 进一步判断e.p是不是被别的地方已经修改为非nil了
p = atomic.LoadPointer(&e.p)
// 如果p为expunged,说明key在其他地方已经被删除了,需要进一步判断处理,这里先终止处理
if p == expunged {
return nil, false, false
}
// e.p已经被其他地方设置值了,这里直接返回已设置的值
if p != nil {
return *(*interface{})(p), true, true
}
}
}
再回头看下sync.Map的数据结构,无论是dirty还是read中的m它们都是内建的map,并且这个map的value是*entry类型。entry的定义如下
type entry struct {
p unsafe.Pointer // *interface{}
}
可以看到, entry是对unsafe.Pointer做了一层包装,这里标题中说的p就是entry中的p. p有三种状态,分别是nil, expunged, 正常状态(指向一个有效的value地址)
为什么搞这么复杂,只有nil和正常状态两种状态可以吗?可以的,之所以搞这么复杂,是为了尽可能榨干性能,提升程序的性能。下面通过图解的方式分析p中的expunged,看完之后你就很清楚的明白expunged是干啥的,为什么有expunged状态。将read和dirty理解为两个集合,key是在read还是dirty中,理解为key是在集合read和还是在集合dirty中。这里重点是梳理清楚p的状态,所以图中只画了key,不关心value细节。每个集合中的一个小方块代表一个key。不同颜色的小方块代表的含义不同,具体含义如下:
对于一个新的sync.Map,开始向里面添加两个元素,key分别为a和b.得到集合状态如下图所示,此时read是空的,dirty中有两个元素。
然后进行两次查询元素操作,因为read为空,两次查询都是从dirty中获取到的,misses未命中计数达到dirty的长度,会将dirty提升为read,并将旧dirty清空,所以得到如下集合状态,read集合中有两个元素,dirty集合是空的。
然后执行删除(key为a)元素操作,将a对应的e.p设置为nil,标记为删除状态,并不是直接delete掉。此时dirty中没有元素,read.amended为false,所以无需对dirty做任何处理。得到的集合状态如下图所示。
在上图的状态的基础上,向里面添加新元素c,因为c在read和dirty中都不存在,需要将其添加到dirty中(注意,添加新元素都是添加在dirty中)。在添加c到dirty之前,需要将read中非删除的元素(key为b)拷贝一份到dirty中,并将read中删除的元素即e.p=nil修改为e.p=expunged状态。得到的集合状态如下图所示。
好了,现在可以分析为什么有e.p=expunged这个状态了,对比上面两张图,可以看到他们的差别是一个dirty是空的,另一个dirty是非空的。现在向里面重新添加回元素a的时候,对于key为a对应的e.p=nil状态,直接在read中添加,这个过程是不需要进行加锁的。但是对于key为a对应的e.p=expunged状态,需要在read和dirty中都添加,这个过程是需要进行加锁的。经过上面的分析,我们也就明白了,为什么要搞一个expunged状态,是为了dirty为空的时候,直接对read进行操作不用加锁,提升程序性能。
理解sync.Map就是要搞清楚read和dirty中元素的分布情况,什么情况下元素在read中什么情况下元素在dirty中,记住一句话,「只要dirty不是nil,dirty中拥有所有的元素,这个时候最全的数据以dirty为准,只要dirty为nil,所有的元素都在read中,这个时候最全的数据以read为准」。下面对sync.Map一些要点进行一个总结。
Go sync.Map 看一看[1]深入理解Go-sync.Map原理剖析[2]
[1]
Go sync.Map 看一看: https://segmentfault.com/a/1190000018657984
[2]
深入理解Go-sync.Map原理剖析: https://segmentfault.com/a/1190000020325763