• go语言 sync.Map原理


            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{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

            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))
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    在这里插入图片描述

     

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    修改或新增数据逻辑:

    /*
    	修改或新增键值对
    */
    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
    		}
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    删除逻辑:

    /*
    	删除键对应的值
    */
    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
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

     

    5、总结

    • 普通map在扩容时会有并发问题
    • sync.Map使用了两个map,分离了扩容问题
    • 不会引发map扩容的操作(查、改)使用read map
    • 可能引发扩容的操作(新增)使用dirty map
    • 读写操作都发送在read map,虽然普通map有并发读写问题,遇到并发读写时会panic,而且读写read map时没有加锁。是因为read map在读写时没有真正的对普通map进行写入,而是只有读取操作,读取entry,然后再读写entry中的指针,使用的是原子操作,因此不会产生并发读写map的问题
    • sync.Map在读多、写多、追加少的情况下性能比较好
  • 相关阅读:
    用于独立系统应用的光伏MPPT铅酸电池充电控制器建模(Simulink实现)
    设计模式-适配器-笔记
    MyBatis获取参数值的两种方式
    人工智能-卷积神经网络(LeNet)
    小程序用vue编写,保存表单出错
    数据机房中智能小母线与列头柜方案的对比与分析
    使用SimPowerSystems并网光伏阵列研究(Simulink实现)
    如何利用PCB创建PCB封装库
    力扣 -- 44. 通配符匹配
    Http Content-type 对照表
  • 原文地址:https://blog.csdn.net/Peerless__/article/details/125506030