golang中map的并发 syncmap详解

golang中map当前版本默认直接并发写会报concurrent map writes 错误

在golang中要实现并发读写的话有三种目前通用的方式:

    1. 使用读写锁sync.RWMutex,在读的时候使用读锁,使用的时候如下代码,效率较低:

 

var counter = struct{
    sync.RWMutex  //读写锁
    m map[string]int
}{m: make(map[string]int)}


counter.RLock() // 读锁定
n := counter.m["some_key"]
counter.RUnlock()


unter.Lock()// 写锁定
counter.m["some_key"]++
counter.Unlock()

    2. 使用golang官方包的syncmap,效率比第一种方式要高。源码在原生包sync.Map,或者github:https://github.com/golang/sync/tree/master/syncmap

    3. 使用其他开源包的concurrentmap,或者自己实现,可以参考java的concurrentmap,使用shard将数据分块,每个锁只针对一个块。

    该文章主要讲syncmap的流程,并在源码中翻译原来注释加入一些中文注释帮助理解,配合源码食用效果更佳。 

总述:  

   

    syncmap是由两个map构成,一个只读readmap,一个写dirtymap。两个map的存储的value都是entry一个指向具体值的指针,如果一个key在read和dirty中同时存在那么修改的时候通过read来原子修改即可,如果value为nil或者expunged代表被删除。

    在代码里面通过原子操作+循环对比来实现lockfree。在操作read的时候使用的是lockfree的方式,操作dirty的时候需要加锁。

    在每次lockfree进入lock之后,由于之前判断情况时候并没有上锁,所以是可能已经改变的,需要再取值判断一次情况。这种情况在整个源码从lockfree到lock时候大量出现。

    

   

    读取

    读的时候优先往read中lockfree读,read中读不到的话再往dirytymap 中lock查找,记录未命中次数,当达到一定次数之后将dirtymap中的数据全部复制到read中。

    读取情况如下如下:

  1. 如果read中存在,返回结果
  2. read中没有,dirty没有生成,返回nil,记一次未命中数
  3. read中没有,返回dirty的结果,记一次为命中数
  4. 未命中数==len(dirty)的话,把read=dirty,dirty=nil

read中没有的意思是指没有对应key,entry指向nil,entry指向expunged

    存储

    在存储数据的时候只往dirtymap中写

    存储情况如下:

  1. 当read中含有key,并且不为expunged时候,直接更新数据。为expunged说明已经存在dirty,并且该key没有进入dirty,需要走下面的情况更新dirty
  2. 当read中含有key,值为expunged时候,先插入entry到dirty中,然后更新值
  3. read中不含有,dirty中含有的话,更新dirty
  4. read中不含有,dirty中不含有,判断是否是dirty还没有生成,没有的话生成dirty,保存数据,已经生成得话直接保存数据

    删除

    删除的话采用滚动删除,流程如下:

  1. dirty还未生成时:从read中将v置为nil
  2. dirty生成时:从read中复制数据,所有expunged不复制,把所有v为nil的置为expunged然后不复制
  3. dirty生成后:如果read中含有的话将v置nil,如果不存在的话,从dirty中删除

    从删除的流程中可以看出,当dirty没有生成时候,read中不含有expunged,被删除的数据全部是nil。dirty中不含有expunged的数据。read中会保留上一轮dirty所有的key,只是在复制到dirty的时候nil和expunged的不复制,从而实现滚动删除key

1.数据结构

    //Map是并发安全的,零Map是空且有效的
type Map struct {
    mu sync.Mutex
        //read包含Map的一部分内容,并且不需要持有锁即可以并发访问
        //read本身load的时候是安全的,但是装载的时候需要持有锁
        //Entries 存储在read中可能没有锁情况下被并发更新,但是更新先前删除的条目需要将条目复制到dirtymap并且用mu锁的情况下更新为未被擦除。
    read atomic.Value // readOnly
        //dirty 包含一部分需要持有锁访问的内容。Dirty map中同时也包含没有被擦去的read entries 来确保快速dirty->read
        // 如果 dirtymap 是nil的话,下次写的时候从read浅复制来初始化,略过过时条目
    dirty map[interface{}]*entry
        //misses 统计从read最后一次更新后(dirty转read)需要锁才能知道key是否存在
        //一旦misses到了一定数量之后就把dirty->read里,下次存数据的时候初始化一个复制出一个新的dirty
    misses int
}

    // readOnly是一个不可变的结构,以原子方式存储在Map.read字段中.
type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}

    //expunged 是一个任意指针,用于标记从dirymap已删除的条目
var expunged = unsafe.Pointer(new(interface{}))

type entry struct {
        // p points to the interface{} value stored for the entry.
        //p 指向 value 存在 entry中的interface{}
        // If p == nil, the entry has been deleted and m.dirty == nil.
        //如果p==nil,entry已经被删除,并且m.dirty==nil(最近一次dirty->read后没有插入操作,尚未未初始化)
        // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry is missing from m.dirty.
        // 如果p==expunged,entry已经被删除,m.dirty!=nil(已经初始化),并且m.dirty中没有这个entry(已经删除)
        // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty!= nil, in m.dirty[key].
        //否则的话,entry 有效并且记录在 read[key]中,如果dirty不为空的话,也在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.
        //entry 可以被删除通过原子替换为nil,当dirty被创建后,原子替换为expunged 并且在dirty中置空
        // 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!=expunged 可以通过原子替换更新entry关联值,p==expunged,先设置dirty[key] = e 之后再更新 entry的关联值
	
p unsafe.Pointer // *interface{}
}

2.存储数据

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
	    //原子获取read
    read, _ := m.read.Load().(readOnly)
        //如果read中有,并且没有被擦除的话,直接更新指针
        //read中没有,需要插入
        //被擦除说明dirty已经初始化,并且部在dirty中,不能直接更新,需要dirty插入
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
	    //上锁流程
    m.mu.Lock()
        //进入lock需要把lockfree中的情况再次判断
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        //存在,说明被擦除了,先expunged变为unexpunged(nil或者其他值),这时候已经可以并发更新了,返回失败的话说明这个时候已经被先前并发程序置为unexpunged了,同时意味着dirty中加入了entry
        if e.unexpungeLocked() {
		    //把dirty中加入entry
            m.dirty[key] = e
        }
		//entry赋值
        e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
	//read中没有,dirty中有的话直接替换entry值即可
        e.storeLocked(&value)
} else {
	//如果read的修订标志是false的话,需要初始化dirtymap
        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.
			//初始化dirtymap
            m.dirtyLocked()
			//使用原子替换
            m.read.Store(readOnly{m: read.m, amended: true})
        }
		//值插入
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}




// tryStore stores a value if the entry has not been expunged.
// If the entry is expunged, tryStore returns false and leaves the entry unchanged.
// 尝试存储value,如果entry还没被擦除的话。如果已经被擦除的话返回false,值不变。被擦除的话再,插入需要插入到dirty,否则的话直接更新e.p即可,dirty持有的是指针
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
        }
    }
}



// unexpungeLocked 确保entry不是expunged,在mu解锁之前要保证entry被加入到dirty中,返回false的话说明已经不是expunged(已经初始化dirtymap)
func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

// storeLocked unconditionally stores a value to the entry.
// The entry must be known not to be expunged.
//
func (e *entry) storeLocked(i *interface{}) {
    atomic.StorePointer(&e.p, unsafe.Pointer(i))
}


//初始化dirtymap
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 {
        //已经为expunged,为nil的话换为expunged,这两种情况不复制进dirty
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

//如果为expunged或者nil返回true,并且把nil换为expunged
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
}

3.读取数据

// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	//原子加载只读
    read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
//如果read中没有并且dirtymap已经被初始化的话,从dirtymap中找
    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.)
		
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        //重新判断,因为在锁前read可能已经经历过一轮变化
        if !ok && read.amended {
			  e, ok = m.dirty[key]
			//无论条目是否存在,记录一次未命中,miss次数和>=len(dirty)的话,将dirty提升为read
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}

	//无论条目是否存在,记录一次未命中,miss次数和>=len(dirty)的话,将dirty提升为read
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
}

4.删除代码

// Delete deletes the value for a key.

func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    //如果不存在,并且dirty已经加载的话删除dirty中的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()
    }
}

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
        }
    }
}

5.Range和LoadOrStore

LoadOrStore:如果有数据的话返回,没有的话存储,总体思路和上面差不多

Range:遍历,如果dirty有的话加锁便利dirty,如果dirty没有的话遍历read

 

 

如果有没有表达清楚或者有误的地方欢迎斧正,码字不易,要是对您有所帮助的话还请点个赞

 

你可能感兴趣的:(golang)