sync.Map实现原理

1,595 阅读4分钟

以为你是独一无二的,谁知竟然偷偷又多了一个

golang中的哈希数据结构是map,在项目中用到map存储k(模型ID)v(发送数据令牌桶)信息,当从上游获取到一条数据,根据模型ID找到对应的令牌桶获取令牌,这就是读的过程,当模型配置修改的时候,需要清除map中存储的模型信息等到下一个该模型的数据到来再添加,这就是写的过程。

golang中的map是不带锁的,支持并发读,却不支持读写并发,如果同时进行读与写,会报错panic(fatal error: concurrent map writes),那么如何解决呢?一种方法是采用的是加锁,可以加mutex或者读写锁,第二种就是sync.map,它支持了并发读写操作。

数据结构

如下所示,map中有read(读)和dirty(写)两种,操作dirty的时候需要加锁,misses用来记录从dirty读的次数,当超过一定次数后dirty中的数据会同步到read中。read中有amended字段来标识同一条记录read中没有dirty中有的情况。expunge代表dirty中被删掉的情况。

type Map struct {
    mu Mutex // 操作dirty时候上的锁
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int // 从dirty读加1,超过一定次数dirty同步到read
}

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
    m       map[interface{}]*entry
    amended bool // 如果dirty中有的read中没有的,标记为true
}

// expunged is an arbitrary pointer that marks entries which have been deleted
// from the dirty map.
var expunged = unsafe.Pointer(new(interface{}))

type entry struct {
    // 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.
    
    p unsafe.Pointer // *interface{}
}

来张图:

三个源码解读(场景需要连起来考虑)

Load

简单来说,load因此会从read和dirty两个中拿,如果read有且dirty中没有修改过,判断该条记录的状态如果是nil或者expunged(dirty中被删掉)则返回获取不到,其他状态返回read中的值;如果read中没有且dirty中修改,就加锁并从dirty中拿,并把missed字段+1,同步dirty到read,dirty=nil.

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        // 可能锁等待的时候read中已经有该key了,所以做二次检查
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            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() //misses计数+1,且如果misses小于dirty的长度,dirty同步到read,dirty=nil
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load() 
}

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
}

Store

如果read中存在key且尝试更新成功(p不是expaunged状态,即在dirty中被删掉),返回即可 否则加锁,并再次检查read中有没有该key,有可能加锁的时候read中已经被同步了该key了。 如果read中有key而之前没有,说明从dirty中同步过来一波,如果p是expunged,就把p设置成nil并且dirty中加入entry 如果read中没有dirty中有,直接存储 如果read中没有dirty中也没有,如果read和dirty中是一样的没有修改过,如果dirty为nil,同步read到dirty,存储到dirty,并且修改read的属性字段amend设置成修改过true

func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() { //如果entry.p是expaunged,就设置成nil
			m.dirty[key] = e
		}
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
		if !read.amended {
			m.dirtyLocked() // 如果dirty为nil,read同步到dirty
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

// 尝试存储,如果entry.p不是expunged就存储,是的话返回false
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
		}
	}
}

func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}
    // dirty为nil
	read, _ := m.read.Load().(readOnly)
	m.dirty = make(map[interface{}]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() { // 如果p不为expunged
			m.dirty[k] = e
		}
	}
}

Delete

如果read中不存在key且read和dirty中不一致,上锁,再次检查read,如果还是如此,删除dirty中的key 如果read中存在key,如果entry.p为nil或者expunged,返回false,如果不是,entry.p设置成nil,标记删除

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

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

总结

sync.Map中有read与dirty,操作dirty需要锁

每次判断完read有无key之后进行进行加锁操作后还需要再次判断read有无key,防止在此时间内进行了从dirty同步到read操作,然后操作dirty

load场景中主要从read读,没有再从dirty读,同时会计数,如果数量超过一定量,数据从dirty同步到read

其中有两处read和dirty互相同步的地方,在store场景主要存到dirty, 如果dirty中有直接更新,当dirty为nil的时候会把read中不是expunged状态的同步到dirty;dirty什么时候为nil呢,在load的时候从dirty中读的次数太多的时候会把dirty同步到read,并且dirty=nil

删除为标记删除,把entry.p=nil来标记

适用场景

读多写少,读多都从read读不需要加锁,并且如果从dirty读有概率会要从dirty拷贝数据到read中