不积跬步,无以至千里

golang 并发安全的map学习


最近做项目,涉及到对一个map做并发读写,为了避免并发问题,每次操作的时候会添加读写锁。下午有一点时间,想着找下看golang有没有并发安全的map,于是就找到了sync.Map,那么问题来了,自己手工加读写锁跟使用sync.Map除了多些代码之外,有没有其他性能上的区别呢?为了尽量提高并发读写,一个考虑是对key做分段加锁,这样可以同时对多个key做操作。但是具体怎么实现呢?学习下

Map包含的元素

type Map struct {
    //一个互斥锁
	mu Mutex

    //一个原子类(cas更新),存储可以不需要锁就可以做并发读取的元素
	read atomic.Value // readOnly

    //dirty包含的元素需要使用排它锁进行访问
	dirty map[interface{}]*entry

	//从readMap获取不到元素的次数,根据这个读缺失的次数来判断是否要把dirty元素写入到readMap
	misses int
}

从Map的定义可以看到,golang的并发安全的Map实现原理是:把已经写入并且未做修改的部分和做了修改的部分拆开,没有做修改的部分可以被多个现成读取,而一旦做了插入或者更新,这部分数据会存到dirty中,对dirty的读写操作需要使用排它锁

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,以免在这期间,dirty中的数据转移到了dirty
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			//这里不管是否在dirty中找到指定的元素,都会对miss+1
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

看下写操作

func (m *Map) Store(key, value interface{}) {
    //由于read的元素使用了cas更新,所以对于read中包含的entryKey,可以直接做更新,这样的更新操作会更加轻量(对于已经删除的元素,read中的entry会被置为expunged表示已删除)
	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()
}
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)//删除dirty中的元素
		}
		m.mu.Unlock()
	}
	if ok {
        //把entry置为nil
		e.delete()
	}
}

从实现来看,sync.Map适合读多写少的情况,更适合更新操作。由于每次插入的时候都需要添加排它锁,所以sync.Map不太适用于需要频繁插入的应用场景,这种情况自己添加读写锁来实现。