【Go夯实基础】sync.Map的实现原理

1、数据结构

  • Map
  type Map struct {
    // 该锁用来保护dirty
    mu Mutex
    
    // 存读的数据,因为是atomic.value类型,只读类型,所以它的读是并发安全的
    read atomic.Value // readOnly
    
    //包含最新的写入的数据,并且在写的时候,会把read 中未被删除的数据
    //拷贝到该dirty中,因为是普通的map存在并发安全问题,需要用到上面的mu字段
    dirty map[interface{}]*entry
    
    // 从read读数据的时候,会将该字段+1,当等于len(dirty)的时候,
    //会将dirty拷贝到read中,这是性能提升的关键
    misses int
}

复制代码
  • readOnly
type readOnly struct {
    //readOnly
    m  map[interface{}]*entry
    
    // 如果Map.dirty的数据和m中的数据不一致,为true
    amended bool 
}
复制代码
  • entry
type entry struct {
    //可见value是个指针类型,虽然read和dirty存在冗余情况(amended=false),但是由于是指针类型,存储的空间应该不是问题
    p unsafe.Pointer // *interface{}
}
复制代码

2、API

2.1 Delete

func (m *Map) Delete(key interface{}) {
    
    //根据key,先行访问read
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    
    //如果read中没有,并且dirty中有新元素,那么就去dirty中去找
    if !ok && read.amended {
        m.mu.Lock()
        
        //这是双检查(上面的if判断和锁不是一个原子性操作)
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        
        //双检查完成之后,如果还是一样的情况下
        if !ok && read.amended {
            //直接删除
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
    
    //双检查完成之后,dirty提升了
    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中没有,并且dirty中有新元素,什么时候会出现这种情况?
    • 查找(更新)时,第三分支(read、dirty都没有的情况下),为了重建dirty,会将read拷贝到dirty上,并且在dirty上面插入

2.2 Store

func(m *Map) Store(key, value interface{}) {
    
    // 如果m.read存在这个key,并且没有被标记删除,则尝试更新
    // 仅仅是尝试更新,trySrore里面如果发现被标记删除会return false
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    
    // 如果read不存在或者已经被标记删除
    m.mu.Lock()
    
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
    //情况一、read存在但是entry被标记expunge,则表明
    //dirty里即使有,也是脏值了,使得readhedirty重新指向相同的e,
    //最后在更新e
        if e.unexpungeLocked() { 
            //加入dirty中
            m.dirty[key] = e
        }
        //更新value值
        e.storeLocked(&value) 
        
    //情况二、dirty 存在该key,更新
    } else if e, ok := m.dirty[key]; ok { 
        e.storeLocked(&value)
        
    //情况三、read 和dirty都没有,新添加一条
    } else {
        //dirty中没有新的数据,往dirty中增加第一个新键
        if !read.amended { 
            //将read中未删除的数据加入到dirty中
            m.dirtyLocked() 
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        //将read复制到dirty之后,并且仅在dirty新增
        m.dirty[key] = newEntry(value) 
    }
    m.mu.Unlock()
}

//将read中未删除的数据加入到dirty中
func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }
    read, _ := m.read.Load().(readOnly)
    m.dirty = make(map[interface{}]*entry, len(read.m))
    //read如果较大的话,可能影响性能
    for k, e := range read.m {
    //通过此次操作,dirty中的元素都是未被删除的,可见expunge的元素不在dirty中
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}
//判断entry是否被标记删除,并且将标记为nil的entry更新标记为expunge
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        // 将已经删除标记为nil的数据标记为expunged
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}
//对entry 尝试更新
func (e *entry) tryStore(i *interface{}) bool {
	p := atomic.LoadPointer(&e.p)
	if p == expunged {
		return false
	}
	for {
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
		p = atomic.LoadPointer(&e.p)
		if p == expunged {
			return false
		}
	}
}
//read里 将标记为expunge的更新为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
	return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
//更新entry
func (e *entry) storeLocked(i *interface{}) {
	atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
复制代码
  • 情况一中,为什么read为expuned时,dirty要恢复?
    • 根据delete的实现可知,建立删除标记的实现是将map[key]指向nil,但是dirt[key]还是原先的那个指针(*entry)
    • 等待他们都指向同一个东西的时候(尽管是nil),最后在同一赋值就好了
    • 这个过程参考图示

2.3 Load

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    
    //因read只读,线程安全,先查看是否满足条件
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    
    //如果read没有,并且dirty有新数据,那从dirty中查找,由于dirty是普通map,线程不安全,这个时候用到互斥锁了
    if !ok && read.amended {
        m.mu.Lock()
        
        // 双重检查
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        
        // 如果read中还是不存在,并且dirty中有新数据
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // mssLocked()函数是sync.Map 性能得以保证的重要函数,
            //目的讲有锁的dirty数据,替换到只读线程安全的read里
            m.missLocked()
        }
        m.mu.Unlock()
    }
    
    //这里的ok,是查找完dirty[key]之后的ok
    if !ok {
        return nil, false
    }
    return e.load()
}

//dirty 提升至read 关键函数,当misses 经过多次因为load之后,大小等于len(dirty)时候,讲dirty替换到read里,以此达到性能提升。
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	//原子操作,耗时很小
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}
复制代码

3、总结

  • 为了理解实际实现,我们需要明白的是,无论是read还是dirty,他们存储的都是值的地址,而且他们是共享地址的。也就是说所有对read的无锁增删改查都会同步反馈在dirty上。这一点非常重要,否则你无法理解为什么增删改查没有经过dirty而dirty却始终反映最新值

  • read访问不需要加锁,dirty要

  • 具体来说,应用有以下的属性,那么可以考虑使用,否则更加建议使用RWMutex或者Mutex结合map的方案

    • 如果写入的key是稳定的(极少)
    • 如果不同goroutine对key的访问是不同的

猜你喜欢

转载自juejin.im/post/7031468077111836680
今日推荐