sync.map源码解析

golang - sync.map源码解析

go语言原生的map并不是线程安全的一种数据结果,如果想要达到安全则需要使用锁,如果map比较大,则加锁解锁代价相对比较大,常见的做法拆分map,使用key值hash的方式进行小规模的锁操作,前者性能影响较大,后者使用锁较多,容易出错

go在1.9之后提供了sync.Map,一种并发安全的map数据结构

操作方式跟常见的map大同小异,只是在遍历的时候操作不太一样

func main() {
	var m sync.Map
	// 1. 写入
	m.Store("test", 18)

	// 2. 读取
	age, _ := m.Load("test")
	fmt.Println(age.(int))

	// 3. 遍历
	m.Range(func(key, value interface{}) bool {
		name := key.(string)
		age := value.(int)
		fmt.Println(name, age)
		return true
	})

	// 4. 删除
	m.Delete("test")
	age, ok := m.Load("test")
	fmt.Println(age, ok)

	// 5. 读取或写入
	m.LoadOrStore("test", 100)
	age, _ = m.Load("test")
	fmt.Println(age)
}

这里面涉及到一个点在于sync.map适用的场景在于读多写少的情况,如果应用场景写入比较多,则会导致read缓冲读取失败,需要加锁,就会导致冲突增多,同时会提升misses(下面会介绍misses的概念),导致dirty提升为read,这是一个O(n)的过程导致性能会大幅度降低

看下官网给出的sync.map的源码
type Map struct {
	// 当涉及到dirty数据的操作的时候,需要使用这个锁
	mu Mutex
	
	// 一个只读的数据结构,因为只读,所以不会有读写冲突。
	// 所以从这个数据中读取总是安全的。
	// 可以并发地读。但如果需要更新 read,则需要加锁保护。对于 read 中存储的 entry 字段,可能会被并发地 CAS 更新。但是如果要更新一个之前已被删除的 entry,则需要先将其状态从 expunged 改为 nil,再拷贝到 dirty 中,然后再更新
	read atomic.Value // 数据对象为readOnly
	
	// dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。
	// 对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。
	// 当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。
	dirty map[interface{}]*entry
	
	// 当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,
	// 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。
	misses int
}
真正存储 key/value 的是 read 和 dirty 字段。read 使用 atomic.Value,这是 lock-free 的基础,保证 load/store 的原子性。dirty 则直接用了一个原始的 map,对于它的 load/store 操作需要加锁。

read 字段里实际上是存储的是:

// readOnly 主要用于存储,通过院子操作存储在Map.read里面的元素
type readOnly struct {
	// read 的map数据,用户存储所有read的数据
	m       map[interface{}]*entry

	// 如果当前元素在dirty中但是没有在read中,则该值为true,元素状态的一种标识符
	amended bool 
}

在dirty和read中元素侧存储数据结构都为 entry

// entry 为map.dirty中存储的具体元素值
type entry struct {
	// p有是三种状态
	// nil 调用delete的时候,删除,将read中元素设置为nil
	// expunged 这也是一种删除状态,只有在read中存在而在dirty中没有,这种情况下降read复制到dirty中,复制的时候先将nil标记为expunged,然后不将其复制到dirty(在创建新的key的时候,如果read中存在,而dirty为空,则需要把read复制到dirty中)
	p unsafe.Pointer // *interface{}
}

map的数据结构示意图
sync.map源码解析_第1张图片
接下来看下整个的sync.map(以下简称map)的详细介绍
这是map的使用整体流程图,简单就是说写更新dirty,读从read中读,如果read中没有,则从dirty中读取
sync.map源码解析_第2张图片
更新函数Store

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
	// 如果 read map 中存在该 key  则尝试直接更改(由于修改的是 entry 内部的 pointer,因此 dirty map 也可见)
	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() {
			// 如果 read map 中存在该 key,但 p == expunged,则说明 m.dirty != nil 并且 m.dirty 中不存在该 key 值 此时:
			//    a. 将 p 的状态由 expunged  更改为 nil
			//    b. dirty map 插入 key
			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,则:
		//	  a. 如果 dirty map 为空,则需要创建 dirty map,并从 read map 中拷贝未删除的元素到新创建的 dirty map
		//    b. 更新 amended 字段,标识 dirty map 中存在 read map 中没有的 key
		//    c. 将 kv 写入 dirty map 中,read 不变
		if !read.amended {
		    // 到这里就意味着,当前的 key 是第一次被加到 dirty map 中。
			// store 之前先判断一下 dirty map 是否为空,如果为空,就把 read map 浅拷贝一次。
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		// 写入新 key,在 dirty 中存储 value
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

整体过程

  1. 如果在 read 里能够找到待存储的 key,并且对应的 entry 的 p 值不为 expunged,也就是没被删除时,直接更新对应的 entry 即可。
  2. 如果 read 中没有这个 key,则加锁进行下面的添加操作
  3. 再次在 read 中查找是否存在这个 key,也就是 double check 一下,这也是 lock-free 编程里的常见套路。如果 read 中存在该 key,但 p == expunged,说明 m.dirty != nil 并且 m.dirty 中不存在该 key 值 此时: a. 将 p 的状态由 expunged 更改为 nil;b. dirty map 插入 key。然后,直接更新对应的 value。
  4. 如果 read 中没有此 key,那就查看 dirty 中是否有此 key,如果有,则直接更新对应的 value,这时 read 中还是没有此 key
  5. 如果 read 和 dirty 中都不存在该 key,则:a. 如果 dirty 为空,则需要创建 dirty,并从 read 中拷贝未被删除的元素,对于已经删除的元素更正状态为expunge;b. 更新 amended 字段,标识 dirty map 中存在 read map 中没有的 key;c. 将 k-v 写入 dirty map 中,read.m 不变。最后,更新此 key 对应的 value

流程图
sync.map源码解析_第3张图片
查询函数Load

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	// 因read只读,线程安全,优先读取
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	// 如果没在 read 中找到,并且 amended 为 true,即 dirty 中存在 read 中没有的 key
	if !ok && read.amended {
		m.mu.Lock() // dirty map 不是线程安全的,所以需要加上互斥锁
		// double check。避免在上锁的过程中 dirty map 提升为 read map。
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		// 仍然没有在 read 中找到这个 key,并且 amended 为 true
		if !ok && read.amended {
			e, ok = m.dirty[key] // 从 dirty 中找
			// 不管 dirty 中有没有找到,都要"记一笔",因为在 dirty 提升为 read 之前,都会进入这条路径
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok { // 如果没找到,返回空,false
		return nil, false
	}
	return e.load()
}
整体过程
  1. 第一步在read中查找,如果找到了直接使用load方法,取出其中的值
  2. 如果没有找到且amended为false,说明dirty为空,则直接返回空和false
  3. 如果read中没有,且amended为true,说明dirty中可能存在当前查询的key,采用double check的方式再dirty中进行查询,无论有没有在dirty中是否找到都需要更新misses的值(函数missLocked的功能)
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	// dirty map 晋升
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}
直接将 misses 的值加 1,表示一次未命中,如果 misses 值小于 m.dirty 的长度,就直接返回。否则,将 m.dirty 晋升为 read,并清空 dirty,清空 misses 计数值。这样,之前一段时间新加入的 key 都会进入到 read 中,从而能够提升 read 的命中率。

entry的load方法

func (e *entry) load() (value interface{}, ok bool) {
	p := atomic.LoadPointer(&e.p)
	//对于 nil 和 expunged 状态的 entry,直接返回 ok=false;否则,将 p 转成 interface{} 返回。
	if p == nil || p == expunged {
		return nil, false
	}
	return *(*interface{})(p), true
}

流程图
sync.map源码解析_第4张图片
删除函数Delete

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	// 如果 read 中没有这个 key,则去dirty中寻找,这里使用的amended,当read和dirty不同时为true,说明dirty中有read的数据
	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 中删除这个 key
		}
		m.mu.Unlock()
	}
	if ok {
	    // 如果read中存在该key,则将该value 赋值nil(采用标记的方式删除!)
		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
        }
    }
}

整体过程

先从read中进行查找,如果有则直接执行delete,将p置为nil,这样read和dirty都能看到这个变化,如果read中没有找到,切dirty不为空,就需要从dirty中进行删除,从dirty中也是更新entry的状态。
如果当前可以在read和dirty中同时存在,则删除只是做一个标记,将p置为nil,而如果仅在dirty中存在时,会直接删除可以,原因在于如果两者同时存在,仅做删除标记,在下次查询的时候,仍然需要先去read查找,之后再去dirty里面进行寻找删除,增大成本

流程图
sync.map源码解析_第5张图片
函数LoadOrStore
该函数结合了Load和Store功能,如果map中存在则返回key对应的value,否则存入map,执行顺序是先执行Load查找key是否存在,之后更新key对应的value,因为LoadOrStore可以并发执行,所以效率相对比较高

函数Range

func (m *Map) Range(f func(key, value interface{}) bool) {
	read, _ := m.read.Load().(readOnly)
	if read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		if read.amended {
			read = readOnly{m: m.dirty}
			m.read.Store(read)
			m.dirty = nil
			m.misses = 0
		}
		m.mu.Unlock()
	}

	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
}

Range遍历当前调用时刻的map中的所有k-v,将他们传递给函数f,进行遍历操作,常见做法如下

    //遍历sync.Map, 要求输入一个func作为参数
    f := func(k, v interface{}) bool {
        //这个函数的入参、出参的类型都已经固定,不能修改
        //可以在函数体内编写自己的代码,调用map中的k,v
 
            fmt.Println(k,v)
            return true
        }
    m.Range(f)
总结
  1. sync.map 是线程安全的,读取,插入,删除也都保持着常数级的时间复杂度。
  2. 通过读写分离,降低锁时间来提高效率,适用于读多写少的场景。
  3. Range 操作需要提供一个函数,参数是 k,v,返回值是一个布尔值:f func(key, value interface{}) bool。
  4. 调用 Load 或 LoadOrStore 函数时,如果在 read 中没有找到 key,则会将 misses 值原子地增加 1,当 misses 增加到和 dirty 的长度相等时,会将 dirty 提升为 read。
  5. 新写入的 key 会保存到 dirty 中,如果这时 dirty 为 nil,就会先新创建一个 dirty,并将 read 中未被删除的元素拷贝到 dirty
  6. 当 dirty 为 nil 的时候,read 就代表 map 所有的数据;当 dirty 不为 nil 的时候,dirty 才代表 map 所有的数据

你可能感兴趣的:(golang,服务端,学习,go,golang)