go中的sync.Map

Go 1.9中的sync.Map提供了线程安全的map,它的优点总结如下:(网上找的)

1.空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
2.使用只读数据(read),避免读写冲突。
3.动态调整,miss次数多了之后,将dirty数据提升为read。
4.double-checking。
5.延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。

6.优先从read读取、更新、删除,因为对read的读取不需要锁。

其实当我们读完它内部实现后就能很好地理解以上好处了。

先从Map的数据结构看起

type Map struct {
	mu Mutex
	read atomic.Value // readOnly
	dirty map[interface{}]*entry
	misses int
}

Mutex加锁的工具,read一个只读的数据结构,所以对它的读总是线程安全的。dirty是map结构,它包含整个Map中的最新的entries,它不是线程安全的,为了保证多线程安全的情况下操作它,需要对它加锁;当dirty为空时,下一次写操作会复制read字段中未删除的数据到dirty。misses计数器,当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一, 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。

    其中read atomic.Value中存的是readOnly数据结构,我们来看下readOnly

type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
}

m无非存数据,amended为true的话说明,dirty数组包含一些新的entries,却存没有在readOnly中。

因为这里数据是只读的,所以读数据时候优先读read数据,若read中没有,则去dirty中读,读到则misses加一。

Map中还一个expunged指针,指向被删除的键值对。Map中的entry结构体无非存有指向value的指针

var expunged = unsafe.Pointer(new(interface{}))

// An entry is a slot in the map corresponding to a particular key.
type entry struct {
	p unsafe.Pointer // *interface{}
}

既然是Map,那我们先看下如何存数据,Store(key, value interfaces{})

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() {
			// 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)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
		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()
}

先看这个情况:第一次new() Map后,里面肯定没有任何数据,第一次Store传入键值对到Map中。

首先通过atomic.Value的Load方法,加载readOnly到read中,此时read中肯定没有任何数据,于是我们要从dirty中取数据,由于它不是线程安全,所以调用mutex锁工具lock,于是读dirty中,也找不到,则判断下amended是否为false(确保是不是第一次往dirty中加数据),由于readOnly是刚创建的其ammended值为false,所以此时方法调用进入m.dirtyLocked()

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

该方法创建dirty,把readOnly的(未被删除的)键值对复制到dirty中,此时readOnly为空。

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
}

创建完dirty后,会创建新的readOnly赋值给read,此时amended为true;最后在dirty中添加键值对,解锁。

情况二:如果此时并不是第一次,并且一开始就在readOnly中找到。

此时调用e.tryStore(&Value);

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

先判断取到的value是不是已经被删除的,如果是则return false;否则,调用cas替换原value的地址值指向新的value,则完成对read中的键值对的更新。如果在更新的过程中,value值被删除则return false;

情况三:如果在read中没找到,在dirty中找到,则加锁,加锁后还是要再load read(以防此时dirty晋升为read),从read中读到被标记删除的value则cas更新dirty中对应key的value设为nil,最后调用storePionter更改value的值。

func (e *entry) storeLocked(i *interface{}) {
	atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

我们再来看Load()方法吧,读数据。

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()
		// 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]
		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()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}
老样子,先从read中找,如果找到则返回value对应的数据。如果没找到,需要判断 amended是否为true,false的话说明dirty中没有多余数据,那找不到。如果为true,则去dirty中找,操作dirty需要先加锁,先判断read中有无(防止dirty晋升成read),找到返回,没找到再找dirty,如果找到那么给misses加一。
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
}
如果misses长度大于等于dirty的长度,则晋升dirty为read,这步骤复杂度为O1,因为无非指针替换,再把dirty更新为nil,misses更新为0;

猜你喜欢

转载自blog.csdn.net/panxj856856/article/details/80363808