并发 - sync.Map

前言

        Go语言中map是一种无须的键值对集合,可以通过key来快速检索数据。Go语言自带的标准的map类型是并发读安全的,但并发写不安全。实现一个并发读写安全的map,只需要定义一个带有RWMutex锁和map类型结构体即可。除此之外,Go语言(1.9以上版本)标准库提供了一个并发安全的集合结构体sync.Map,直接直接安全使用,无需额外加锁。

        因此,本篇文章就从源码的角度分析sync.Map实现并发读写安全的原理。


一、 Map

        在讲解sync.Map之前,我们先简单看map并发写的问题以及怎么实现并发写的安全。

1.Map并发写

package maing

// 定义变量
var sMap = make(map[interface{}]interface{}, 0)

func main() {
	for i := 0; i < 100000; i += 1 {
		go func(key int) {
			sMap[key] = key
		}(i)
	}
}

        运行上述代码会出现报错:"fatal error: concurrent map writes" ,开启多个协程并发写入导致。如何实现并发安全map写入呢,请看下面改良代码。

2.Map并发写(加锁)

package main

import (
	"sync"
)

// 定义带RWMutex和map的结构体
type safeMap = struct {
	sync.RWMutex
	m map[interface{}]interface{}
}

// 初始化定义变量
var sMap = safeMap{m: make(map[interface{}]interface{})}

func main() {
	for i := 0; i < 100000; i += 1 {
		go func(key int) {
			sMap.Lock()
			sMap.m[key] = key
			sMap.Unlock()
		}(i)
	}
}

        运行上传代码程序正常执行,解决map并发安全写入问题。但这种解决方式相当于串行写,在大并发写入情况会导致性能问题。所以我们用Go的官方包sync.Map,能更好的实现需求。

二、sync.Map设计思想

1.时间换空间

        之前map读操作中,采用传统大锁,其锁竞争十分激烈,造成性能极具下降。sync.Map采用空间换时间,冗余数据结构,减少执行时间,提升性能。

        sync.Map中冗余数据结构是read和dirty,二者存放的都是key-entry,entry其实是一个指针,指向value;read和dirty各自维护一套key,key指向的都是同一个value,即修改了entry,对read和dirty都是可见的。

2.读写分离

        sync.map通过read和dirty两个字段实现读写分离,将读和写操作分开,避免了读写操作。

3.双检查机制

        当read不符合要求就操作dirty,此时会加锁,加锁后再次判断read是否符合要求(read可能在加锁期间更新,符合要求了),符合则操作后续内容,如不符合再操作dirty。

4.延迟删除

        在删除操作中,只需要将要删除的k-v打上标记,这样可以让delete操作先返回,减少时间消耗;后面提升dirty时,一次性删除标记的k-v。

5.read优先

        需要进行读取、删除、更新操作时,需要优先操作read,因为read无锁,是在read中得不到结果,再去操作dirty。

6.状态机制

        entry的指针p,是有状态的。分三种状态:nil、expunged(指向被删除的元素)、正常。

三、sync.Map源码

1.数据结构

        sync.Map并发安全性的实现是典型的空间换时间的思想,提供了read和dirty这两个数据结构,用来降低加锁对性能的负面影响。其结构体如下:

// Map map结构体
type Map struct {
	mu Mutex // 当涉及到dirty数据的操作,需要使用此锁
	read atomic.Value // readOnly
	dirty map[interface{}]*entry // dirty数据结构
	misses int // 计数器
}

// readOnly 是一个不可变结构,以原子方式存储在Map.read字段中
type readOnly struct {
 m       map[interface{}]*entry
 amended bool // amended=true表示dirty map存在新值和m不一致
}


// entry 键值对中的值结构体
type entry struct {
 p unsafe.Pointer // *interface{}
}
说明 类型 作用
mu Mutex 加锁作用,操作dirty时使用
read atomic.Value 只读的数据,实际数据类型为readOnly
dirty map[interface{}]*entry 包括read中的数据,还有新增的数据;misses计数达到一定值,会将dirty提升为read,重置dirty和misses

 

说明 类型 作用
m map[interface{}]*entry map结构
amended bool amended=true:Map.dirty和m不一致;否则相同

 

说明 类型 作用
p unsafe.Pointer sync.map中key和value是分开存放,key通过内置map指向entry,entry通过指针,指向value实际内存地址

2.sync.map查找

// Load 查询key值是否存在
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 在read中查找key
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
    // 在read中未找到,且read和dirty数据不一致,开始加锁
	if !ok && read.amended {
         // 加锁,准备操作dirty中数据
		m.mu.Lock()
		// 双检查机制,再次在read中查找key(此时可能read从dirty中更新了数据)
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
        // 在read中还是没有找到,且read和dirty数据仍然不一致
		if !ok && read.amended {
            // 在dirty中查找key
			e, ok = m.dirty[key]
            // read不命中次数+1,到达阈值后,为避免read命中率太低,会从dirty中更新read数据 
			m.missLocked() 
		}
        // 解锁
		m.mu.Unlock() 
	}
    // 未查找到key,说明key在map中确实不存在,返回nil
	if !ok {
		return nil, false
	}
    // 查找到key,返回value
	return e.load()
}

// missLocked read miss计数+1
func (m *Map) missLocked() {
	m.misses++ // read 没命中次数+1
    // 计数 < dirty数量,无需操作
	if m.misses < len(m.dirty) {
		return
	}
    // dirty提升为read,该操作是原子操作
	m.read.Store(readOnly{m: m.dirty})
    // dirty重置
	m.dirty = nil
     // miss计数重置
	m.misses = 0
}

整体过程:

  1. 在read中查找key,若找到直接使用load方法,返回其值
  2. 若read中未查找且amended=false(read和dirty数据一致,都找不到key),直接返回nil
  3. 若read中未查找且amended=true(read和dirty数据一致,可能存在key);采用双检机制,先加锁,再去read中查找;若查找到key,返回其值;如果查找到且amended=true,则去dirty中查找;无论dirty是否找到,miss计数+1
  4. misses计数+1,表示未命中次数+1;如果misses值小于dirty的长度,则直接返回;否则将dirty提升为read,清空dirty数据,清空misses计数,从而提升read命中率

3.sync.map新增|更新

// Store 新增|更新 k-v
func (m *Map) Store(key, value interface{}) {
    // 在read查找key,且entry没有被标记删除,则更新,否则返回false
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
    // 加锁,准备操作dirty
	m.mu.Lock()
    // 双检查机制,再次在read中查找key
	read, _ = m.read.Load().(readOnly)
    // read中存在key
	if e, ok := read.m[key]; ok {
         // 未被标记成删除
		if e.unexpungeLocked() {
            // 加入dirty,此处是指针
			m.dirty[key] = e
		}
        // 更新entry.p=value(read map和dirty map指向同一个entry)
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok { // 若read map中不存在key,但dirty map存在该 key,直接写入更新entry(read map中仍然没有key)
		e.storeLocked(&value)
	} else {
        // 若read map和dirty map都不存在该key
		if !read.amended {
            // 此时key是第一次被加到dirty map中;store之前先判断dirty map是否为空,若为空,就把read map拷贝到dirty map
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
        // 在dirty中存储k-v
		m.dirty[key] = newEntry(value) 
	}
    // 解锁,不再操作dirty
	m.mu.Unlock() 
}

整体流程:

  1. 若在read中能找到key,并且对应的entry的p值不为expunged,即k-v没有被删除,直接更新对应的entry
  2. 若第1步未成功,不是read中无此key,就是read中key已标记删除,此时需要加锁开始后续操作
  3. 采用双检查机制,再次在read中查找key;若read中存在key,但p==expunged,说明m.dirty!=nil且m.dirty不存在此key;此时将p的状态由expunged改为nil,dirty.map插入k-v
  4. 若read中无key,在dirty中查找key。若有,则直接更新对应value值;若无,此时read中无此key
  5. 若read和dirty中都不存在key,分几种情况:①.若dirty为空,则需要创建dirty,并从read中复制未被删除的元素;②.更新amended字段,标识dirty map中存在read map中没有的key;③.将k-v写入dirty map中,read.m不变。最后更新k-v

3.sync.map删除

// Delete 删除k-v
func (m *Map) Delete(key interface{}) {
	m.LoadAndDelete(key)
}

// LoadAndDelete 
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
	// 在read中查找key
    read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
    // 在read中无此key,且read和dirty中数据不同
	if !ok && read.amended {
        // 加锁,准备操作dirty
		m.mu.Lock() 
        // 双检查机制,在read中查询key
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
        // 在read中未查找到key,且read和dirty中数据不相同,则在dirty中删除key
		if !ok && read.amended {
			e, ok = m.dirty[key]
			delete(m.dirty, key)

			m.missLocked()
		}
        // 解锁,不再操作dirty
		m.mu.Unlock()  
	}
    // 若read中找到key,则将key对应value置为nil
	if ok {
        // 将entry.p标记为nil,数据并没有实际删除
        // 真正删除数据并被置为expunged,是在Store的tryExpungeLocked中
		return e.delete()
	}
	return nil, false
}

// delete 删除value
func (e *entry) delete() (value interface{}, ok bool) {
	for {
        // 加载指针(原子操作)
		p := atomic.LoadPointer(&e.p)
		if p == nil || p == expunged { // p==nil或p==expunged,则删除失败
			return nil, false
		}
        // 将p指向nil
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return *(*interface{})(p), true
		}
	}
}

整体流程:

  1. 在read中查找,若有进行delete,将p置为nil,此时read和dirty都能看到变化;若read中没有找到,且dirty不为空,就需要从dirty中进行删除,也是更新entry的状态
  2. 如果当前可以在read和dirty中同事存在

4.sync.map遍历

// Range 回调方式依次遍历k-v
func (m *Map) Range(f func(key, value interface{}) bool) {
	// 1.加载read,read.amended=true,则read与dirty数据不一致(dirty有新数据),则提升dirty,然后再遍历
	read, _ := m.read.Load().(readOnly)
    if read.amended {
        // 加锁,准备操作dirty
		m.mu.Lock() 
        // 双检查机制,再次加载read
		read, _ = m.read.Load().(readOnly) 
        // read与dirty数据不一致(dirty有新数据),提升dirty为read,重置dirty和miss计数器
		if read.amended {
			read = readOnly{m: m.dirty}
			m.read.Store(read)
			m.dirty = nil
			m.misses = 0
		}
        // 解锁,不再操作dirty
		m.mu.Unlock()
	}
    // 2.回调方式遍历read
	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
}

整体流程:

  1. 首先加载read,若read.amended=false(read与dirty数据一致),回调方式遍历k-v
  2. 若read.amended=true(read与dirty数据不一致),加锁,采用双检查机制,再次读取read,如果read与dirty不一致,dirty提升read,重置dirty和miss计数器;
  3. 最后通过回调方式遍历read

总结

  • sync.map是读写安全的
  • 通过读写分离,降低锁的时间提升效率,适合读多写少场景

你可能感兴趣的:(Go,golang)