sync.Map
sync.Map
众所周知,golang的map只读是线程安全的,同时写线程不安全,虽然加锁可以解决并发写的问题,但是加锁往往会有效率问题,为了提升效率,官方在go的1.9版本推出了sync.Map。
sync.map
是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。
sync.map
的零值是有效的,并且零值是一个空的 map。在第一次使用之后,不允许被拷贝.
1. 如何使用
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 1. 写入
m.Store("zhao", 18)
m.Store("qian", 20)
m.Store("li", 30)
// 2. 读取
age1, _ := m.Load("zhao")
fmt.Println(age1.(int))
// 3. 遍历
m.Range(func(key, value interface{}) bool {
name := key.(string)
age := value.(int)
fmt.Println(name, age)
return true
})
// 4. 删除
m.Delete("zhao")
age2, ok := m.Load("zhao")
fmt.Println(age2, ok)
// 5. 读取并删除
age4, loaded := m.LoadAndDelete("li")
fmt.Println(age4, loaded)
// 6. 读取或写入
age3, flag := m.LoadOrStore("qian", 100)
fmt.Println(age3, flag)
}
2. 原理
sync.Map
主要采取“空间换时间”的策略,来支持并发读写,并且相对于加锁来说,降低了对性能的影响。具体方式是增加两个冗余的数据结构,分别是:read和dirty。
Sync.Map的核心数据结构
type Map struct {
// 当涉及到dirty数据的操作的时候,需要使用这个锁
mu Mutex
// 一个只读的数据结构,因为只读,所以不会有读写冲突,所以从这个数据中读取总是安全的。
// 实际上,实际也会更新这个数据的entries,如果entry是未删除的(unexpunged), 并不需要加锁。
// 如果entry已经被删除了,需要加锁,以便更新dirty数据。
read atomic.Value // readOnly
// dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,
// 但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),
// 有些数据还可能没有移动到read字段中。对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争,
// 当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。
dirty map[interface{}]*entry
// 当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,
// 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。
misses int
}
说明 | 类型 | 作用 |
---|---|---|
mu | Mutex | 加锁。保护后文的dirty字段 |
read | atomic.Value | 存读的数据。因为是atomic.Value类型,只读,所以并发是安全的。实际存的是readOnly的数据结构。 |
misses | int | 计数作用。每次从read中读失败,则计数+1。 |
dirty | map[interface{}]*entry | 包含最新写入的数据。当misses计数达到一定值,将其赋值给read。 |
readOnly的数据结构:
type readOnly struct {
m map[interface{}]*entry
amended bool
}
说明 | 类型 | 作用 |
---|---|---|
m | map[interface{}]*entry | 单纯的map结构 |
amended | bool | Map.dirty的数据和这里的 m 中的数据不一样的时候,为true |
entry的数据结构:
type entry struct {
//可见value是个指针类型,虽然read和dirty存在冗余情况(amended=false),但是由于是指针类型,存储的空间应该不是问题
p unsafe.Pointer // *interface{}
}
3.增删改查
3.1 Load
加载方法,也就是提供一个键key
,查找对应的值value
,如果不存在,通过ok
反映:
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
// 1.首先从m.read中得到只读readOnly,从它的map中查找,不需要加锁
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
// 2. 如果没找到,并且m.dirty中有新数据,需要从m.dirty查找,这个时候需要加锁
if !ok && read.amended {
m.mu.Lock()
// 双检查,避免加锁的时候m.dirty提升为m.read,这个时候m.read可能被替换了。
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
// 如果m.read中还是不存在,并且m.dirty中有新数据
if !ok && read.amended {
// 从m.dirty查找
e, ok = m.dirty[key]
// 不管m.dirty中存不存在,都将misses计数加一
// missLocked()中满足条件后就会提升m.dirty
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
首先从m.read
中加载,不存在的情况下,并且m.dirty
中有新数据,加锁,然后从m.dirty
中加载。
可以看到,如果我们查询的键值正好存在于m.read
中,无须加锁,直接返回,理论上性能优异。即使不存在于m.read
中,经过miss
几次之后,m.dirty
会被提升为m.read
,又会从m.read
中查找。所以对于更新/增加较少,加载存在的key很多的case,性能基本和无锁的map类似。
下面看看m.dirty
是如何被提升的。 missLocked
方法中可能会将m.dirty
提升。
func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}
上面的最后三行代码就是提升m.dirty
的,很简单的将m.dirty
作为readOnly
的m
字段,原子更新m.read
。提升后m.dirty
、m.misses
重置, 并且m.read.amended
为false。
3.2 Store
这个方法是更新或者新增一个entry。
func (m *Map) Store(key, value interface{}) {
// 如果m.read存在这个键,并且这个entry没有被标记删除,尝试直接存储。
// 因为m.dirty也指向这个entry,所以m.dirty也保持最新的entry。
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 如果`m.read`不存在或者已经被标记删除
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() { //标记成未被删除
m.dirty[key] = e //m.dirty中不存在这个键,所以加入m.dirty
}
e.storeLocked(&value) //更新
} else if e, ok := m.dirty[key]; ok { // m.dirty存在这个键,更新
e.storeLocked(&value)
} else { //新键值
if !read.amended { //m.dirty中没有新的数据,往m.dirty中增加第一个新键
m.dirtyLocked() //从m.read中复制未删除的数据
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value) //将这个entry加入到m.dirty中
}
m.mu.Unlock()
}
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 {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
// 将已经删除标记为nil的数据标记为expunged
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
你可以看到,以上操作都是先从操作m.read
开始的,不满足条件再加锁,然后操作m.dirty
。
Store
可能会在某种情况下(初始化或者m.dirty刚被提升后)从m.read
中复制数据,如果这个时候m.read
中数据量非常大,可能会影响性能。
3.3 Delete
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
e.delete()
}
}
同样,删除操作还是从m.read
中开始, 如果这个entry不存在于m.read
中,并且m.dirty
中有新数据,则加锁尝试从m.dirty
中删除。
注意,还是要双检查的。 从m.dirty
中直接删除即可,就当它没存在过,但是如果是从m.read
中删除,并不会直接删除,而是打标记:
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
// 已标记为删除
if p == nil || p == expunged {
return false
}
// 原子操作,e.p标记为nil
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
3.4 Range
因为for ... range map
是内建的语言特性,所以没有办法使用for range
遍历sync.Map
, 但是可以使用它的Range
方法,通过回调的方式遍历。
func (m *Map) Range(f func(key, value interface{}) bool) {
read, _ := m.read.Load().(readOnly)
// 如果m.dirty中有新数据,则提升m.dirty,然后在遍历
if read.amended {
//提升m.dirty
m.mu.Lock()
read, _ = m.read.Load().(readOnly) //双检查
if read.amended {
read = readOnly{m: m.dirty}
m.read.Store(read)
m.dirty = nil
m.misses = 0
}
m.mu.Unlock()
}
// 遍历, for range是安全的
for k, e := range read.m {
v, ok := e.load()
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
4. 总结
sync.map
是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。- 通过读写分离,降低加锁时间来提高效率,适用于读多写少的场景。
- Range 操作需要提供一个函数,参数是
k,v
,返回值是一个布尔值:f func(key, value interface{}) bool
。 - 调用 Load 或 LoadOrStore 函数时,如果在 read 中没有找到 key,则会将 misses 值原子地增加 1,当 misses 增加到和 dirty 的长度相等时,会将 dirty 提升为 read。以期减少“读 miss”。
- 新写入的 key 会保存到 dirty 中,如果这时 dirty 为 nil,就会先新创建一个 dirty,并将 read 中未被删除的元素拷贝到 dirty。
- 当 dirty 为 nil 的时候,read 就代表 map 所有的数据;当 dirty 不为 nil 的时候,dirty 才代表 map 所有的数据。