Go同步原语之sync/Map

19 篇文章 0 订阅
4 篇文章 0 订阅

基本用法

我们都知道gomap是并发不安全的,当几个goruotine同时对一个map进行读写操作时,就会出现并发写问题:

package main

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
	m := make(map[int]int)
	go func() {
		i := 0
		for ; i < 1000; i++ {
			m[i] = i
		}
        wg.Done()
	}()
	go func() {
		i := 0
		for ; i < 1000; i++ {
			_ = m[i]
		}
        wg.Done()
	}()
	wg.Wait()
	fmt.Println("run end\n")
}

该程序不断对 m变量进行赋值与取值,理论上只要在多核cpu下,如果子goroutine和主goroutine同时在运行,就会出现问题。我们不妨用go自带的-race 来检测下,可以运行 go run -race main.go

go run -race .\main.go
==================
WARNING: DATA RACE
Read at 0x00c00001e030 by goroutine 7:
  runtime.mapaccess1_fast64()
      C:/Go/src/runtime/map_fast64.go:12 +0x0
  main.main.func2()
      D:/wamp64/www/golang/src/main/main.go:23 +0x54
..................

通过检测,我们可以发现,存在data race,即数据竞争问题。

遇到数据竞争问题,如何解决呢?

有人说这简单,加锁解决,加锁固然可以解决,那我们用加锁解决试试:

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	var mu sync.RWMutex
	m := make(map[int]int)
	wg.Add(2)
	go func() {
		i := 0
		for ; i < 1000; i++ {
			mu.Lock()
			m[i] = i
			mu.Unlock()
		}
		wg.Done()
	}()
	go func() {
		i := 0
		for ; i < 1000; i++ {
			mu.RLock()
			_ = m[i]
			mu.RUnlock()
		}
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("run end\n")
}

加锁的确解决了问题,但是大家都知道加锁会消耗大量开销,在高并发场景下,锁的争用会造成系统性能的下降。

那么就用go内置的方法sync.Map,它可以解决并发问题。

sync.Mapmap不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false

sync.Map 对外提供了以下常用方法:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {...}    //获取数据
func (m *Map) Store(key, value interface{}) {...}						  //储存数据
func (m *Map) Delete(key interface{}) {...}								  //删除数据
func (m *Map) Range(f func(key, value interface{}) bool) {...}			  //遍历数据
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {......}
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {......}

基本使用示例:

import (
	"fmt"
	"sync"
)

func main() {
	//声明一个sync.Map变量m
	m := sync.Map{}

	//存数据
	m.Store(1, 1)
	m.Store(2, 2)
	m.Store(3, 3)

	//取数据
	k1, ok := m.Load(1)
	fmt.Println(k1, ok)

	//遍历map
	m.Range(func(key, value interface{}) bool {
		fmt.Println(key, value)
		return true
	})

	//删除map数据
	m.Delete(1)
}

基础使用熟悉后,我们用 sync.map 解决上面的并发问题:

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	m := sync.Map{}
	wg.Add(2)
	go func() {
		i := 0
		for ; i < 1000; i++ {
			m.Store(i, i)
		}
		wg.Done()
	}()
	go func() {
		i := 0
		for ; i < 1000; i++ {
			 _, _ = m.Load(i)
		}
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("run end\n")
}

sync.Map 确实可以解决并发map问题,但是它在读多写少的情况下,比较适合,可以保证并发安全,同时又不需要锁的开销,在写多读少的情况下反而可能会更差,主要是因为它的设计。

所以在合适的地方使用合适的方案,是解决并发问题根本办法。总结起来:

  • 并发量少的情况下可以使用锁
  • 并发量大并且读多写少的情况可以使用 sync.Map
  • 并发量大并伴有大量写操作,可以考虑 对内部map进行分片,降低锁粒度,从而达到最少的锁等待时间方案

结构体

sync.Map的核心数据结构如下:

//go 1.20.3  path: src/sync/map.go
type any = interface{}

type Map struct {
  mu Mutex
  read atomic.Pointer[readOnly]
  dirty map[any]*entry
  misses int
}

结构体字段分析如下:

  • mu 互斥锁 ,当涉及到脏数据(dirty)操作时候,需要使用这个锁;

  • dirty 是一个非线程安全的原始 map,提供读写分离的写功能,用Mutex锁进行保护。

  • read 一个atomic.Pointer指针类型,其值指向是 readOnly类型, 并发是安全的, readOnly类型对应的是一个只读map,数据结构定义如下:

    type readOnly struct {
    	m       map[any]*entry
    	amended bool
    }
    
    type entry struct {
      p atomic.Pointer[any]
    }
    

    字段解释如下:

    • readOnly.m 是一个map结构,结构跟 sync.Map.dirty 一致,其键为一个任意数值,其值指向一个*interface{}
    • readOnly.amended 为一个判断 sync.Map.read 中的数据是否存在缺失,需要通过 dirty map 兜底,值分两种情况:
      • amendedtrue,表示sync.Map.dirty中存在sync.Map.read未包含的数据,需要再查询sync.Map.dirty中是否包含该key
      • amendedfalse,表示sync.Map.dirty为空,需要重构sync.Map.dirty数据;
    • entry.p 是一个interface{}类型指针,可以为任何数据,其拥有三种状态:
      • p == nil 时,说明这个键值对已被删除(软删除),read mapdirty map 底层的 map 结构仍存在 key-entry 对,但在逻辑上该 key-entry 对已经被删除,因此无法被用户查询到;
      • p == expunged 时,说明这条entry键值对已被删除(硬删除), sync.Map.dirty 中彻底没有这个key-entry 对;
      • 其他值则表示p 指向一个正常的。
  • misses 每当从 read 中读取失败,都会将 misses 的计数值加 1,当加到一定阈值以后,需要将 dirty 提升为 read,以期减少 miss 的情形。

如下图表示sync.Map的字段之间结构体的大致互相关系:

image-20230905105515558

通过上述关系图可以看出 sync.Map.read.msync.Map.dirty对应的数据结构体是一致的,那它们之间是否有什么关系呢? sync.Map为什么这么设计呢?

这个我们从源码中去寻找这些问题的答案。

源码分析

sync.Map.Load

sync.Map.Load是负责查询和读取map数据的函数, 该函数读取指定 key 返回 value,直接开始扒源码:

//go 1.20.3  path:   src/sync/map.go

func (m *Map) Load(key any) (value any, ok bool) {
	// 读取只读的map,即sync.Map.read中的map
	read := m.loadReadOnly()
	// 从只读的map中读取key对应的value
	e, ok := read.m[key]
	if !ok && read.amended { // 如果没有找到值,并且sync.Map.dirty中有数据,则执行下面的逻辑
		m.mu.Lock()             // 加锁
		read = m.loadReadOnly() // 重新读取只读的map
		e, ok = read.m[key]     // 从只读的map中读取key对应的value

		// 如果没有找到值,并且sync.Map.dirty中有数据,则执行下面的逻辑
		if !ok && read.amended {
			// 从sync.Map.dirty中读取key对应的value
			e, ok = m.dirty[key]
			m.missLocked() // 统计miss次数
		}
		m.mu.Unlock() // 解锁
	}
	//如果还是没有找到值,则返回nil
	if !ok {
		return nil, false
	}
	// 返回找到的值
	return e.load()
}

func (m *Map) loadReadOnly() readOnly {
	if p := m.read.Load(); p != nil {
		return *p
	}
	return readOnly{}
}

func (x *Pointer[T]) Load() *T { return (*T)(LoadPointer(&x.v)) }

func (e *entry) load() (value any, ok bool) {
	p := e.p.Load()
	// 如果p为nil或者p为expunged,则返回nil
	if p == nil || p == expunged {
		return nil, false
	}
	return *p, true
}

sync.Map.Load 整体流程:

  • 首先进入 fast path(快捷方式),直接在 sync.Map.read 中找key,如果找到了直接调用 entryload 方法,取出其中的值;
  • 如果 sync.Map.read 中没有这个 key,则分情况处理:
    • 如果 amendedfase,说明 sync.Map.dirty为空(在missLocked处理的时候,会将amended设置为false,此时sync.Map.dirty会被清空),直接返回空和 false
    • 如果 amendedtrue,说明 sync.Map.dirty 中可能存在我们要找的 key,当然要先上锁,再尝试去 sync.Map.dirty 中查找。在这之前,仍然有一个 double check 的操作,再去sync.Map.read查找一次这个key。若还是没有在 sync.Map.read 中找到,那么就从 sync.Map.dirty 中找。不管 sync.Map.dirty 中有没有找到,都要记一笔misses+1,因为在 sync.Map.dirty 被提升为 sync.Map.read 之前,都会进入这条路径;

再来看看sync.Map.Load中调用关于misses操作的函数missLocked

//go 1.20.3  path:   src/sync/map.go
func (m *Map) missLocked() {
	//miss次数加1
	m.misses++
	// 如果miss次数小于sync.Map.dirty中的数据量,则直接返回
	if m.misses < len(m.dirty) {
		return
	}

	// 如果miss次数大于等于sync.Map.dirty中的数据量,则执行下面的逻辑

	m.read.Store(&readOnly{m: m.dirty}) // 将sync.Map.dirty中的数据赋值给sync.Map.read
	m.dirty = nil                       // 清空sync.Map.dirty
	m.misses = 0                        // 将miss次数置为0
}

该代码主要作用是统计将 misses+1,此时根据misses值分两种情况处理:

  • 如果 misses 值小于 sync.Map.dirty 长度,则直接返回;
  • 如果 misses 大于或等于sync.Map.dirty的长度,则将sync.Map.dirty中的数据赋值给sync.Map.read,然后清空的sync.Map.dirty, 同时misses设置为0。

这就是说 sync.Map.read好比整个sync.Map的一个“高速缓存”,当goroutinesync.Map中读数据时,sync.Map会首先查看read这个缓存层是否有用户需要的数据(key是否命中),如果有(key命中),则通过原子操作将数据读取并返回,这是sync.Map推荐的快路径(fast path),也是sync.Map的读性能极高的原因。

下面用一张图来完整的归纳总结下sync.Map.Load函数流程:

image-20230905143948490

sync.Map.Store

sync.Map.Store 函数主要负责更新数据, 更新指定keyvalue,假如该key 已经存在则更新 value, 不存在则插入新数据。

sync.Map.Store 函数源码如下:

//go 1.20.3  path:   src/sync/map.go

func (m *Map) Store(key, value any) {
	//调用swap方法
	_, _ = m.Swap(key, value)
}

func (m *Map) Swap(key, value any) (previous any, loaded bool) {
  // 返回值 previous表示替换前的值,loaded表示key是否存在
  
	// 读取只读的map,即sync.Map.read中的map
	read := m.loadReadOnly()
  
	if e, ok := read.m[key]; ok {  //read.m中查询key是否存在,如果存在执行该逻辑

		// 执行替换操作,将值替换为value
		if v, ok := e.trySwap(&value); ok {
			//如果v为nil,则表示key之前已经被删除了,直接返回nil,false
			if v == nil {
				return nil, false
			}
			//如果v不为nil,则表示key存在,返回替换前的值
			return *v, true
		}
	}

	//运行到此处,表示key在read.m中不存在,执行下面的逻辑

	m.mu.Lock()             // 加锁
	read = m.loadReadOnly() // 重新读取只读的map,即sync.Map.read中的map

	if e, ok := read.m[key]; ok { //从read.m查询key是否存在,如果存在,则执行下面的逻辑

		// 如果read值域中entry已经删除被标记为expunged,则表明dirty没有该key,则可添加到dirty中,并更新entry
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}

		//调用swapLocked方法执行替换操作
		if v := e.swapLocked(&value); v != nil {
			loaded = true //标记loaded为true,表示key存在
			previous = *v //将前值赋予previous
		}
	} else if e, ok := m.dirty[key]; ok { //如果read值域中entry不存在,则从dirty中查询key是否存在,如果存在,则执行下面的逻辑
		//调用swapLocked方法执行替换操作
		if v := e.swapLocked(&value); v != nil {
			loaded = true
			previous = *v
		}
	} else { //如果read值域中entry不存在,dirty中也不存在,则执行下面的逻辑
    //如果read.amended为false,表示中的map数据缺失
		if !read.amended { 
			//调用dirtyLocked方法,将read.m中的数据复制到sync.Map.dirty中
			m.dirtyLocked()
			//将read.amended置为true
			m.read.Store(&readOnly{m: read.m, amended: true})
		}
		//将key和value添加到sync.Map.dirty中
		m.dirty[key] = newEntry(value)
	}

	m.mu.Unlock()           // 解锁
	return previous, loaded //返回
}


sync.Map.Store 函数中还有一些辅助函数,在此处给出:

//go 1.20.3  path:   src/sync/map.go
type any = interface{}
var expunged = new(any)

// trySwap 尝试将entry的值替换为i
func (e *entry) trySwap(i *any) (*any, bool) {
	for {
		p := e.p.Load()
		if p == expunged {
			return nil, false
		}
		if e.p.CompareAndSwap(p, i) {
			return p, true
		}
	}
}

// unexpungeLocked 将entry的值从expunged状态恢复到正常状态
func (e *entry) unexpungeLocked() (wasExpunged bool) {
	return e.p.CompareAndSwap(expunged, nil)
}

// swapLocked 将entry的值替换为i
func (e *entry) swapLocked(i *any) *any {
	return e.p.Swap(i)
}

// dirtyLocked 将read.m中的数据复制到sync.Map.dirty中
func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}
	read := m.loadReadOnly()
	m.dirty = make(map[any]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

sync.Map.Store 函数整体流程:

  1. 如果在 read 里能够找到待存储的 key,并且对应的 entryp 值不为 expunged,也就是没被删除时,直接更新对应的 entry 即可;

  2. 如果read 中没有这个 key,则先加锁,再double check 一下,再检查一次read中是否存在这个key,这也是 lock-free 编程里的常见套路。如果 read 中存在该 key,但 p == expunged,说明 m.dirty != nil 并且 m.dirty 中不存在该 key 值 此时:

    • p 的状态由 expunged 更改为 nil,至于为啥要设置为nil,最后总结中会讲到;
    • dirty map 插入 key。然后直接更新对应的 value
  3. 如果 read 中没有此 key,那就查看 dirty 中是否有此 key,如果有,则直接更新对应的 value,这时 read 中还是没有此 key

  4. 最后一步,如果 readdirty 中都不存在该 key,则:

    • 如果 dirty 为空,则需要创建 dirty,并从 read 中拷贝未被删除的(即值不为nil或者expunge的)元素;
    • 更新 amended 字段值为 true,标识 dirty map 中存在 read map 中没有的 key(即后面加入dirty map的新的key-entry);
    • k-v 写入 dirty map 中,read.m 不变;
    • 最后更新此 key 对应的 value
  5. 最后,释放锁,结束赋值过程。

sync.Map.Store 函数整体流程图:

image-20230905175730973

sync.Map.Delete

sync.Map.Delete 函数负责删除Map中的数据,具体代码如下:

//go 1.20.3  path:   src/sync/map.go

func (m *Map) Delete(key any) {
	//调用LoadAndDelete方法,如果返回的loaded为true,则说明删除成功
	m.LoadAndDelete(key)
}

func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
	//获取只读的map,即sync.Map.read
	read := m.loadReadOnly()
	//从read.m中获取key对应的entry
	e, ok := read.m[key]
	if !ok && read.amended { //如果read.m中不存在key对应的entry,并且read.amended为true
		m.mu.Lock() //加锁
		//double check.再次获取只读的map,即sync.Map.read
		read = m.loadReadOnly()
		//再次从read.m中获取key对应的entry
		e, ok = read.m[key]
		if !ok && read.amended { //如果read.m中不存在key对应的entry,并且read.amended为true
			//从sync.Map.dirty中获取key对应的entry
			e, ok = m.dirty[key]
			//从sync.Map.dirty中删除key对应的entry
			delete(m.dirty, key)
			//更新miss字段,如果miss字段大于等于dirty的长度的一半,则将dirty中的数据复制到read.m中
			m.missLocked()
		}
		m.mu.Unlock() //解锁
	}
	//如果read.m中存在key对应的entry,则直接删除
	if ok {
		return e.delete()
	}
	// 返回
	return nil, false
}

相关辅助函数:

//删除entry
func (e *entry) delete() (value any, ok bool) {
	for {
		p := e.p.Load() //获取entry的值
		//如果entry的值为nil或者expunged,则直接返回
		if p == nil || p == expunged {
			return nil, false
		}
		//如果entry的值不为nil或者expunged,则将entry的值替换为nil,并返回entry的值
		if e.p.CompareAndSwap(p, nil) {
			return *p, true
		}
	}
}

sync.Map.Delete 函数基本流程:

  1. 先从 read 里查是否有这个 key,如果有则执行 entry.delete 方法,将 p 置为 nil,这样 readdirty 都能看到这个变化;
  2. 如果没在 read 中找到这个 key,并且 dirty 不为空,那么就要操作 dirty 了,操作之前,还是要先上锁。然后进行 double check,如果仍然没有在 read 里找到此 key,则从 dirty 中删掉这个 key。但不是真正地从 dirty 中删除,而是更新 entry 的状态

sync.Map.Delete 整体流程图:

image-20230906101006526

sync.Map.Range

sync.Map.Range 的参数是一个函数:

f func(key, value interface{}) bool

提供了O(N)的方式遍历sync.Map,用户传入遍历key-value的动作函数,如果函数返回false,则遍历终止;即使在遍历过程中发生key的并发读写操作,每个key也仅会被最多遍历一次。因为 dirty可能包含read中不存在的key的状态,当遍历操作发生时,如果dirtyread存储的有效key的状态不一致,将dirty提升为read

sync.Map.Range 的源码如下:

func (m *Map) Range(f func(key, value any) bool) {
	//获取只读的map,即sync.Map.read
	read := m.loadReadOnly()
	if read.amended { //如果read.amended为true,意味着sync.Map.read中的数据数据过期,需要从sync.Map.dirty中获取数据
		m.mu.Lock() //加锁
		//double check, 再次获取只读的map,即sync.Map.read
		read = m.loadReadOnly()
		if read.amended { //如果read.amended为true,意味着sync.Map.read中的数据数据过期,需要从sync.Map.dirty中获取数据
			//从sync.Map.dirty中获取数据
			read = readOnly{m: m.dirty}
			//更新sync.Map.read
			m.read.Store(&read)
			//将sync.Map.dirty置为nil
			m.dirty = nil
			//将misses置为0
			m.misses = 0
		}
		m.mu.Unlock() //解锁
	}
	//遍历sync.Map.read中的数据
	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) { //调用f
			break
		}
	}
}

上述代码流程比较简单,如下:

  • read包含所有有效元素时,直接遍历read中存储的值;

  • dirty含有read中不存在的元素时,将dirty提升为read,再遍历新read中存储的;

sync.Map.Range 流程图如下:

image-20230906110537767

高并发安全Map的解决方案

orcaman/concurrent-map的适用场景是:反复插入与读取新值,其实现思路是:对go原生map进行分片加锁,降低锁粒度,从而达到最少的锁等待时间(锁冲突)。

concurrent-map源码地址:https://github.com/orcaman/concurrent-map

部分实现源码:

// SHARD_COUNT 分片大小
var SHARD_COUNT = 32

// string:Anything 类型的“线程”安全映射
// // 为了避免锁瓶颈,这个map被嵌入了几个 (SHARD_COUNT) map shards。
type ConcurrentMap[V any] []*ConcurrentMapShared[V]

// 任何映射的“线程”安全字符串
type ConcurrentMapShared[V any] struct {
	items        map[string]V
	sync.RWMutex // 读写互斥锁,保护对内部映射的访问
}

// 创建一个新的并发映射map.
func New[V any]() ConcurrentMap[V] {
	m := make(ConcurrentMap[V], SHARD_COUNT)
	for i := 0; i < SHARD_COUNT; i++ {
		m[i] = &ConcurrentMapShared[V]{items: make(map[string]V)}
	}
	return m
}

函数介绍:

  • Get 方法

    // 先hash拿到key对应的分区号,然后加锁,读取值,最后释放锁和返回
    func (m ConcurrentMap[V]) Get(key string) (V, bool) {
    	// Get shard
    	shard := m.GetShard(key)
    	shard.RLock()
    	// Get item from shard.
    	val, ok := shard.items[key]
    	shard.RUnlock()
    	return val, ok
    }
    
  • Set 方法

    // 先hash拿到key对应的分区号,然后加锁,设置新值,最后释放锁
    func (m ConcurrentMap[V]) Set(key string, value V) {
    	// Get map shard.
    	shard := m.GetShard(key)
    	shard.Lock()
    	shard.items[key] = value
    	shard.Unlock()
    }
    
  • Remove 方法

    // 先hash拿到key对应的分区号,然后加锁,删除key,最后释放锁
    func (m ConcurrentMap[V]) Remove(key string) {
    	// Try to get shard.
    	shard := m.GetShard(key)
    	shard.Lock()
    	delete(shard.items, key)
    	shard.Unlock()
    }
    
  • Count 方法

    // 分别拿到每个分片map中的元素数量,然后汇总后返回
    func (m ConcurrentMap[V]) Count() int {
    	count := 0
    	for i := 0; i < SHARD_COUNT; i++ {
    		shard := m[i]
    		shard.RLock()
    		count += len(shard.items)
    		shard.RUnlock()
    	}
    	return count
    }
    
  • Upsert 方法

    // 先hash拿到key对应的分区号,然后加锁,如果key存在就更新其value,否则插入新的k-v,释放锁并返回
    func (m ConcurrentMap[V]) Upsert(key string, value V, cb UpsertCb[V]) (res V) {
    	shard := m.GetShard(key)
    	shard.Lock()
    	v, ok := shard.items[key]
    	res = cb(ok, v, value)
    	shard.items[key] = res
    	shard.Unlock()
    	return res
    }
    

总结

entry.p 状态

在结构体内容部分介绍过,entry.p 是一个interface{}类型指针,其拥有三种状态:nilexpunged、其他正常值。

nilexpunged有啥区别呢?

其实 nilexpunged都用来表示key值已经被删除了,但区别的是:

  • nil值代表软删除态:read mapdirty map 在物理上仍保有该 key-entry 对,因此倘若此时需要对该 entry 执行写操作,可以直接 CAS 操作;
  • expunged值代表硬删除态:dirty map 中已经没有该 key-entry 对,倘若执行写操作,必须加锁(dirty map 必须含有全量 key-entry 对数据);

设计 expungednil 两种状态的原因,就是为了优化在 dirtyLocked 前,针对同一个 key 先删后写的场景. 通过 expunged 态额外标识出 dirty map 中是否仍具有指向该 entry 的能力,这样能够实现对一部分 nilkey-entry 对的解放,能够基于 CAS 完成这部分内容写入操作而无需加锁。

下列图示演示了 nil 态软删除的数据的基于CAS恢复:

image-20230906145404463

dirty map 和 read map

前面章节已经对这这2map进行了介绍,在源码分析中也讲到了,为了更清晰的说明这2map的作用以及相互关系,这边重点再分析分析。

sync.Map 由两个重要的 map 构成:

  • read map:访问时全程无锁,其内容为 dirty map 的子集;
  • dirty map:是兜底的读写 map,访问时需要加锁;

sync.Map 本质上采取了一种以空间换时间 + 动态调整策略的设计思路 ,希望在读、更新、删频次较高时,更多地采用 CAS 的方式无锁化地完成操作;在写操作频次较高时,则直接了当地采用加锁操作完成。

总体思想,希望能多用 read map,少用 dirty map,因为操作前者无锁,后者需要加锁。如下图所示:

image-20230906153522299

来看看什么情况下会从dirty map复制数据给read map

通过源码阅读,我们在函数 missLocked 中找到了相关操作:

//go 1.20.3  path:   src/sync/map.go
func (m *Map) missLocked() {
	//miss次数加1
	m.misses++
	// 如果miss次数小于sync.Map.dirty中的数据量,则直接返回
	if m.misses < len(m.dirty) {
		return
	}

	// 如果miss次数大于等于sync.Map.dirty中的数据量,则执行下面的逻辑

	m.read.Store(&readOnly{m: m.dirty}) // 将sync.Map.dirty中的数据赋值给sync.Map.read
	m.dirty = nil                       // 清空sync.Map.dirty
	m.misses = 0                        // 将miss次数置为0
}

从代码可以看出,当misses次数(read状态命中失败的次数)大于等于dirty map的数据量时,此时将dirty提升为read,老 dirty 作为新 read,并将新的dirty map设置为nil,将 misses 计时器重置为0。(注意此时m.amended默认设置为false

这么做的是提升下次只读状态read查询的命中率,避免加锁操作。

image-20230906160646029

那又是什么情况下会从read map数据复写给dirty map

通过源码,可以得出当dirty 暂时为空(此时,m.amended应该为false)且接下来一次写操作访问 readmiss,需要进行dirtyLocked操作,在dirtyLocked操作过程中会将read map数据复写给dirty map,来看下dirtyLocked函数代码:

func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}
	read := m.loadReadOnly()
	m.dirty = make(map[any]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

dirtyLocked 流程中,需要对 read 内的元素进行状态更新,因此需要遍历。dirtyLocked 遍历中,会将 read 中未被删除的元素(非 nilexpunged)拷贝到 dirty 中;会将 read 中所有此前被删的元素统一置为 expunged 态。

如下图:

image-20230906170030116

至此,内容就这些吧。

参考资料:

假装懂编程 https://baijiahao.baidu.com/s?id=1705075387433772703&wfr=spider&for=pc

付少华 https://blog.csdn.net/weixin_44014995/article/details/109260782

咖啡色的羊驼 https://blog.csdn.net/u011957758/article/details/96633984

背着电脑去搬砖 https://blog.csdn.net/xzw12138/article/details/109505767

[小徐先生的编程世界](javascript:void(0)😉 https://mp.weixin.qq.com/s/nMuCMA8ONnhs1lsTVMcNgA

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值