Golang源码探究 —— sync.Map

20 篇文章 4 订阅
7 篇文章 2 订阅

        Golang内置的map是不支持并发读写的,它在内部有检测机制,一旦发现并发读写,就会panic。如果需要并发读写map,有三种方案。1、使用map + Mutex 2、使用 map + RWMutex 3、使用sync.Map。前两者的效率在大部分情况下都不如官方提供的sync.Map。接下来来分析一下sync.Map是如何实现并发读写的。

 

1、sync.Map的结构

sync.Map的源码在sync/map.go中, skd版本:1.18

sync.Map的结构用图表示如下:

在这里插入图片描述

源码如下:

type Map struct {
    // 互斥锁
	mu Mutex


    /*
    	read中包含了一个可以并发访问的map,无需对mu加锁就可以访问,读写数据都会经过read map
    	它最终会包含这样一个结构:
    	type readOnly struct {
            m       map[interface{}]*entry
            amended bool     
        }
    */
	read atomic.Value // readOnly

    // dirty map 新增数据,走dirty map
	dirty map[interface{}]*entry

	// 未命中次数
	misses int
}

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
	m       map[any]*entry
	amended bool      // 如果dirty中存在没有在m中的key时为true 
}

type entry struct {
	p unsafe.Pointer // *interface{}
}

        sync.Map结构体中包含了四个字段,其中包含了两个map[interface{}]*entry ,entry中包含了一个unsafe的Pointer,这个指针指向了真正的value值。

 

2、sync.Map读写

2.1 正常读写

Map的正常读写是走的read这个map,从read map中查找k-v。

在这里插入图片描述

2.2 追加问题

        假设我们要追加一个键值对"d":“go”,首先,因为不知道map中有没有这个key,因此需要先在read map中寻找,没有找到就会先对mu加锁,然后在下面的dirty map中追加。

在这里插入图片描述

        追加的时候,要创建一个entry,指针指向真正的值。然后要将amended置为true,表示read中的map已经不完整了,dirty map中有新追加的键值对。但是为什么要这样做?正常已存在的键值的读和修改走的都是上面的read map,而追加则是走的dirty map。因为上面的read map的读和写都不会涉及到map扩容的问题,而追加可能会导致map扩容。

在这里插入图片描述

2.3 追加后读写

        在追加后,我们要读出"d"的值,首先还是要走read这条线,read中找不到,但是amended为true,表示read中的map和dirty map不一致。因此要从dirty map中查找。读完后要把misses加1,表示一次要读的键在上面的map中未命中。随着多次的未命中,msses逐渐增加,当增加到misses == len(dirty)时,就会进行dirty提升。

在这里插入图片描述

2.4 dirty提升

        当misses与dirty map的长度相等时,就无法忍受了。因为多次在read中读取却未命中,还需要再走dirty map,这时候就会进行dirty map的提升。将上面的map干掉,将dirty提升上去,dirty变成新的read map,此时下面的dirty map为nil。后面如果追加的话,会重建dirty map。然后就会进入了初始的循环。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

 

3 sync.Map删除

  • sync.Map的删除相比于查询、修改、新增更麻烦。
  • 删除可以分为正常删除和追加后删除。
  • 提升后,被删的key还需要特殊处理。

3.1 正常删除

        在read map中根据key找到对应的entry,但是不是删除在map中对应的entry,而是将entry中的指针置为nil,这样就没有任何指针指向真正的值了,垃圾回收器就可以进行回收。

在这里插入图片描述

3.2 追加后删除

        假设"d"是我们刚追加上去的,read map和dirty map中的数据是不一致的。如果要删除"d",首先从read map中查找,没有找到,而且amended为true。对mu进行上锁,然后取dirty map中查找,找到后将entry中的指针置为nil。但是追加后删除涉及一个提升的问题,假设删除后,后面要将dirty map提升上来,那么四个键中的其中一个是指向nil的,如果要重建下面的dirty map,是否要重建这个nil的key呢?这个时候就不重建它,而是将指针指向expunged(删除了的意思)。将指针指向expunged就是为了提醒来访问的协程,这个键值对已经被删除了,如果要删除这个键值对的话,之间删除就行了,因为在下面已经没有了。

// expunged is an arbitrary pointer that marks entries which have been deleted
// from the dirty map.
var expunged = unsafe.Pointer(new(any))

在这里插入图片描述

在这里插入图片描述

 

4、源码分析

源码如下:

读取数据的逻辑:

/*
	从Map中读取数据
*/
func (m *Map) Load(key any) (value any, ok bool) {
    // 从atomic.Value中加载数据并断言为readOnly类型
	read, _ := m.read.Load().(readOnly)
    // 直接从read的map中读取
	e, ok := read.m[key]
    // 如果没有找到,并且read.amended为true,说明read map和dirty map中的数据不一致,因此需要再从dirty map中读取
	if !ok && read.amended {
        // 对mu上锁
		m.mu.Lock()
		// 防止当阻塞在lock上的时候,dirty map被提升了,因此需要重新获取一次read map
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
        
		if !ok && read.amended {
            // 访问dirty map
			e, ok = m.dirty[key]
            // 无论是否找到了需要的键值,都记录一个miss
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
    
    // 从entry中获取数据
	return e.load()
}

// 从获取到的entry中的指针读取数据,如果指针为nil或者expunged说明要查找的key-val不存在
func (e *entry) load() (value any, ok bool) {
	p := atomic.LoadPointer(&e.p)
    // 如果p == expunged,说明已经被删除
	if p == nil || p == expunged {
		return nil, false
	}
	return *(*any)(p), true
}

// 记录未命中的次数,如果次数达到dirty map的长度时,提升dirty map
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
    // 如果mmisses == len(m.dirty) 就要提升dirty
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

修改或新增数据逻辑:

/*
	修改或新增键值对
*/
func (m *Map) Store(key, value any) {
    // 获取read
	read, _ := m.read.Load().(readOnly)
    // 先从read map中查询是否存在,如果存在,就是修改操作,调用tryStore修改read map中的数据
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
	
    // 要修改的数据可能在dirty map中,属于2.3的情况 或者是不在dirty map中,那么就是新增k-v操作
    
    // 先上锁
	m.mu.Lock()
    // 再次查询read map防止阻塞在lock期间,dirty map被提升
	read, _ = m.read.Load().(readOnly)
    // ok == true说明dirty map已经被提升,而且要修改的数据在原来的dirty map中
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
            // entry被删除了
			m.dirty[key] = e
		}
		e.storeLocked(&value)
    // 查找dirty map,如果找到,就修改dirty map中的数据    
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
    // 追加数据
	} else {
        // 
		if !read.amended {
            // 第一次新增,此时amended为false,可能是dirty提升后第一次新增,此时需要重建dirty map
			m.dirtyLocked()
            // 修改amended为true
			m.read.Store(readOnly{m: read.m, amended: true})
		}
        // 新增数据
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

/*
	修改read map中的值
*/
func (e *entry) tryStore(i *any) bool {
	for {
		p := atomic.LoadPointer(&e.p)
        // 说明已经被删除
		if p == expunged {
			return false
		}
        // 原子操作,修改值
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
	}
}

/*
	第一次新增,dirty map可能被提升了,如果被提升,那么dirty map为nil,需要重建
*/
func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}
	// 重建dirty map
	read, _ := m.read.Load().(readOnly)
	m.dirty = make(map[any]*entry, len(read.m))
	for k, e := range read.m {
        // 不会添加expunged的k
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

删除逻辑:

/*
	删除键对应的值
*/
func (m *Map) Delete(key any) {
	m.LoadAndDelete(key)
}

/*
	删除键对应的值并返回原值
*/
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
	read, _ := m.read.Load().(readOnly)
    // 判断要删除的key是否在read map中
	e, ok := read.m[key]
    // 不在read map中,并且read map和dirty map中的数据不一致
	if !ok && read.amended {
		m.mu.Lock()
        // 同样的,防止在阻塞期间,dirty map被提升了
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
            // 在dirty map中
			e, ok = m.dirty[key]
            // 直接删除
			delete(m.dirty, key)
			// 记录未命中次数
			m.missLocked()
		}
		m.mu.Unlock()
	}
    // 并没有从read map中删除val,而是将entry中的指针置为nil
	if ok {
		return e.delete()
	}
	return nil, false
}

/*
	将val从entry中删除
*/
func (e *entry) delete() (value any, ok bool) {
	for {
		p := atomic.LoadPointer(&e.p)
        // p == nil说明已经被删除
        // p == expunged 说明原来在dirty map中被删除了,并且原来的dirty map被提升为了read map
		if p == nil || p == expunged {
			return nil, false
		}
        // 将entry中的指针置为nil
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return *(*any)(p), true
		}
	}
}

 

5、总结

  • 普通map在扩容时会有并发问题
  • sync.Map使用了两个map,分离了扩容问题
  • 不会引发map扩容的操作(查、改)使用read map
  • 可能引发扩容的操作(新增)使用dirty map
  • 读写操作都发送在read map,虽然普通map有并发读写问题,遇到并发读写时会panic,而且读写read map时没有加锁。是因为read map在读写时没有真正的对普通map进行写入,而是只有读取操作,读取entry,然后再读写entry中的指针,使用的是原子操作,因此不会产生并发读写map的问题
  • sync.Map在读多、写多、追加少的情况下性能比较好
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
sync.Map 是 Go 语言标准库提供的一种并发安全的字典类型,它可以被多个 goroutine 安全地访问和修改。在多个 goroutine 并发地读写一个 map 时,会出现竞争条件,从而导致数据不一致。而 sync.Map 利用了一些锁的技巧,避免了这种竞争条件的发生,从而实现了高效的并发安全访问。 sync.Map 的 API 非常简单,主要包括以下几个方法: 1. Store(key, value interface{}):将一个键值对存储到 sync.Map 。 2. Load(key interface{}) (value interface{}, ok bool):根据键从 sync.Map 获取对应的值。 3. LoadOrStore(key, value interface{}) (actual interface{}, loaded bool):如果键存在于 sync.Map ,则返回对应的值和 true,否则将键值对存储到 sync.Map 并返回新的值和 false。 4. Delete(key interface{}):从 sync.Map 删除一个键值对。 5. Range(f func(key, value interface{}) bool):遍历 sync.Map 的键值对,并对每一个键值对调用函数 f,如果 f 返回 false,则停止遍历。 下面是一个使用 sync.Map 的简单例子,展示了如何在多个 goroutine 并发地访问和修改 sync.Map: ``` package main import ( "fmt" "sync" ) func main() { var m sync.Map var wg sync.WaitGroup wg.Add(2) // goroutine 1: 向 sync.Map 存储键值对 go func() { defer wg.Done() m.Store("key1", "value1") m.Store("key2", "value2") }() // goroutine 2: 从 sync.Map 加载键值对 go func() { defer wg.Done() if v, ok := m.Load("key1"); ok { fmt.Println("value for key1:", v) } if v, ok := m.Load("key2"); ok { fmt.Println("value for key2:", v) } }() wg.Wait() } ``` 在上面的例子,我们首先创建了一个 sync.Map 对象 m。然后在两个 goroutine 同时访问这个对象,一个 goroutine 向其存储键值对,另一个 goroutine 则从其加载键值对。由于 sync.Map 是并发安全的,所以这两个 goroutine 可以并发地访问和修改 sync.Map,而不会出现竞争条件。 需要注意的是,虽然 sync.Map 是并发安全的,但它并不是用来替代普通的 map 的。如果你只是需要在某个 goroutine 访问和修改一个 map,那么你应该使用普通的 map,因为 sync.Map 的性能会比较差。只有在需要多个 goroutine 并发地访问和修改一个 map 时,才应该考虑使用 sync.Map
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值