【go源码阅读笔记】 - Sync.Map

446 阅读5分钟

为什么需要Sync.Map

首先,map在并发场景下并不是thread free的,所以在操作map的时候需要加锁,相比于加锁,原子操作自然是更节省时间的,所以Go的作者们就引入了Sync.Map

代码示例

package main

import (
   "fmt"
   "sync"
)

func main() {
   var syncMap sync.Map
   var wg sync.WaitGroup

   wg.Add(2)
   go func() {
      wg.Done()
      syncMap.Store("1", 1)
   }()
   go func() {
      wg.Done()
      syncMap.Store("2", 2)
   }()
   wg.Wait()
   syncMap.Range(func(key, value interface{}) bool {
      fmt.Println(key, value)
      return true
   })
}

在上面的例子中,同时开启两个goroutine对sync.Map进行操作,并未出现并发异常,且最终输出了正确的结果

源码剖析

在看核心代码之前,先来看一下相关结构体的定义

type Map struct {
	mu Mutex           // 操作dirty map时需要加锁
	read atomic.Value  // 只读map,这里通过更改read中指针的状态来进行逻辑删除
        // dirty map相当于一个兜底,它里面的key/value对会比read map中要全       
	dirty map[interface{}]*entry  
	misses int         // 当读取数据的时候,会先从read map中去读,如果没有的话,再去dirty   map中查找,此时misses数加1
}

// read对应的存储数据
type readOnly struct {
	m       map[interface{}]*entry
	amended bool // 如果dirty map比read map中存储的数据要全,则该字段的值为true
}

// expunged表示当前的key已经从dirty map中删除,也就是read map中有而dirty map中没有
var expunged = unsafe.Pointer(new(interface{}))

// entry结构体中包含着一个指向map中value的指针 
type entry struct {
	// p points to the interface{} value stored for the entry.
	//
	// If p == nil, the entry has been deleted and m.dirty == nil.
	//
	// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
	// is missing from m.dirty.
	//
	// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
	// != nil, in m.dirty[key].
	//
	// An entry can be deleted by atomic replacement with nil: when m.dirty is
	// next created, it will atomically replace nil with expunged and leave
	// m.dirty[key] unset.
	//
	// An entry's associated value can be updated by atomic replacement, provided
	// p != expunged. If p == expunged, an entry's associated value can be updated
	// only after first setting m.dirty[key] = e so that lookups using the dirty
	// map find the entry.
	p unsafe.Pointer // *interface{}
}

接下来我们分析Sync.Map中的三个核心操作: Store/Load/Delete

Store(存储)

func (m *Map) Store(key, value interface{}) {
        // 首先会从read map中进行读取,如果某个键存在并且值的状态不为expunged的话,存储成功
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
        
        // 否则需要对dirty map进行操作,由于dirty是一个普通的map,所以这里需要加锁
	m.mu.Lock()
        // 如果当前的键在read map中是存在的,并且状态为expunged的话,则说明dirty map != nil 
        // && dirty map中不存在该键值对,首先将状态修改为nil,然后放入dirty map中,之后
        // 进行赋值操作
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			// The entry was previously expunged, which implies that there is a
			// non-nil dirty map and this entry is not in it.
			m.dirty[key] = e
		}
		e.storeLocked(&value)
        // 如果当前的键在dirty map中存在,直接存储
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
        // 如果当前的键在dirty map以及read map中都不存在,如果dirty map为nil,则将read map
        // 中所有不为nil和expunged状态的键值对给dirty map,之后将新的键值对放入dirty map中
		if !read.amended {
			// We're adding the first new key to the dirty map.
			// Make sure it is allocated and mark the read-only map as incomplete.
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

// 在read map中尝试存储,如果entry的状态为expunged的话,则返回false,否则进行CAS操作
func (e *entry) tryStore(i *interface{}) bool {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == expunged {
			return false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
	}
}

// 将expunged状态替换为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
	return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

// 存储相关的值
func (e *entry) storeLocked(i *interface{}) {
	atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

// 将read map中所有状态不为nil以及expunged的键值对放入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 {
		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
}

Load(加载)

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
        // 首先先从read map中找数据
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
        // 如果不存在,并且dirty map不为空
	if !ok && read.amended {
		m.mu.Lock()
		// Avoid reporting a spurious miss if m.dirty got promoted while we were
		// blocked on m.mu. (If further loads of the same key will not miss, it's
		// not worth copying the dirty map for this key.)
                // 进行double check,防止在加锁的过程中read map中有了该key
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
                        // 如果依旧没有,则从dirty map中取数据,并计数miss次数
			e, ok = m.dirty[key]
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
        // 如果没有找到,返回nil
	if !ok {
		return nil, false
	}
	return e.load()
}

// 计数没有命中read map的次数,如果命中次数达到dirty map的长度,则将dirty map中的值统一给read map
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
}

// 加载value值
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
}

Delete(删除)

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
        // 删除操作和加载操作很类似,首先从read map中找对应的key
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
        // 如果没有并且dirty map不为空,则从dirty map中找
	if !ok && read.amended {
		m.mu.Lock()
                // 进行double check,防止在加锁的过程中添加了该key
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
                // 如果依旧没有,则从dirty map中找,并进行删除,同时增加miss的计数值
		if !ok && read.amended {
			e, ok = m.dirty[key]
			delete(m.dirty, key)
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
        // 将该值的状态置为nil,表示已删除
	if ok {
		return e.delete()
	}
	return nil, false
}

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
	m.LoadAndDelete(key)
}

// 删除entry
func (e *entry) delete() (value interface{}, ok bool) {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == nil || p == expunged {
			return nil, false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return *(*interface{})(p), true
		}
	}
}

看完源代码,可以发现,如果写操作很多的话,如果read map中的值不能全部命中,会进行加锁且dirty map会拷贝read map中的键值对,性能方面会大打折扣,所以Sync.Map适用于读多写少的情况