【博客363】Go的并发安全map:sync.Map

内容:记录Go中并发安全的一种map

Go原生map不是并发安全的map

Go的原生map不是并发安全的,在多协程读写同一个map的时候,安全性无法得到保障

在Go的1.9版本之前的解决方案:使用读写锁来避免竞争

//将锁变量与map一起封装成一个并发安全的类型
var Map = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

//读数据
Map.RLock()
n := Map.m["key"]
Map.RUnlock()

//写数据
Map.Lock()
Map.m["key"]=value
Map.Unlock()

在Go的1.9版本之后,引入了并发安全的sync.Map

特点:
1、并发安全

2、通过冗余的手段,冗余两个数据结构(read、dirty)优化加锁对性能的影响

3、内部引入大量double checking(双重检测)的机制

4、动态调整,当read miss次数达到len(dirty)后,将dirty提升为read

5、延迟删除,删除某个key时,先进行逻辑删除(标记),只有dirty提升为read时才真正进行
   物理删除

6、优化read的读取、更新,对read的读取不加锁

sync.Map的数据结构:

    type Map struct {
    //互斥锁,用于dirty数据操作
	mu Mutex

	//只读的数据结构,不会有读写冲突
	// Entries stored in read may be updated concurrently without mu, 
	//but updating a previously-expunged entry requires that the entry
	// be copied to the dirty
	//当entries处于未删除状态(unexpunged),不需要加锁,如果entries已经删除
	//(previously-expunged ),需要加锁,以便下次更新dirty
	
	read atomic.Value // readOnly

	//1、dirty数据包含了当前map最新的entries
	//2、提升dirty为read时不需要一个个复制,而是利用原子操作将这个数据结构作为read
	//的一部分
	//3、对于dirty的操作需要加锁,对它的操作可能面临读写竞争
	//4、dirty为空时,可能是刚初始化或者刚被提升,下一次操作将从read中处于未删除状态
	//的数据复制到dirty中。
	dirty map[interface{}]*entry
	
	//当从read中读取数据时,如果read不包含相应的数据,将尝试加锁从dirty中读取,
	//无论读取是否成功,miss均加一
	//当len(dirty)==misses,将dirty提升变成read
	misses int
}

该数据结构以空间换时间,冗余了read、dirty;dirty中包含read已经逻辑删除的entries,以及新加入的entries;
read对应的DataStruct为:

    type readOnly struct {
	    m       map[interface{}]*entry
	//amended为true时,说明dirty中有readOnly未包含的数据,所以如果从read找不到
	//数据的话,设置该值为true,要进一步到dirty中查找。
	amended bool 
    }

实际上,尽管read、dirty之间互相冗余,但是两者数据之间通过指针指向同一块内存地址,减少了内存的浪费:

    // An entry is a slot in the map corresponding to a particular key.
    type entry struct {
	    p unsafe.Pointer // *interface{}

        //p有三种值:
	    //1、nil: entry已被删除了,并且m.dirty为nil
	    //2、expunged: entry已被删除了,并且m.dirty不为nil,而且这个entry不存在于
	    //m.dirty中
	    //3、正常:entry是一个正常的有用值
    }

readOnly.m和dirty存储的value类型是*entry,它包含一个指针p, 指向用户存储的value值,节约了内存的消耗。

结构图:
在这里插入图片描述
sync.Map各操作示意图:
在这里插入图片描述
在这里插入图片描述
sync.Map各操作源码:

## sync.Map的load函数:加载数据

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    //首先依赖atomic的原子操作Load获取一个只读的readOnly数据结构
	read, _ := m.read.Load().(readOnly)
	//查找readOnly中的数据,不需要加锁。
	e, ok := read.m[key]
	//如果没找到,且amended为true即dirty中有新数据,从dirty中查找,并且加锁。
	if !ok && read.amended {
		m.mu.Lock()
		//再次获取readOnly,double-checking,避免加锁的时候m.dirty被提升为m.read,
		//这个时候m.read已经不对应。
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			//不管e是否存在,misses计数均加一,当满足条件后提升dirty
			m.missLocked()
		}
		m.mu.Unlock()
	}
	//找不到此元素
	if !ok {
		return nil, false
	}
	return e.load()
}

特点:
1、 一个只读的数据结构,因为只读,所以不会有读写冲突。所以从这个数据中读取总是安全的。

2、实际上,实际也会更新这个数据的entries,如果entry是未删除的(unexpunged), 并不
   需要加锁。如果entry已经被删除了,需要加锁,以便更新dirty数据。
   
3、dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,
   虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个
   数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。
   
4、对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。

5、当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的
   数据到这个数据中。

6、当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候
   会将misses加一,

7、当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多
   次。因为操作dirty需要加锁。
    
## missLocked函数:dirty如何提升为read!!!!重要
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	//当满足一定条件时,调用store函数进行提升
	m.read.Store(readOnly{m: m.dirty})
	//m.dirty清空
	m.dirty = nil
	//重新计数
	m.misses = 0
}

## store函数:更新/增加一个entry
func (m *Map) Store(key, value interface{}) {
    //获取readOnly
	read, _ := m.read.Load().(readOnly)
    //如果m.read存在这个键,并且这个entry没有被标记删除,直接存储。
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
    // 如果read不存在或者已经被标记删除,需要操作dirty,则需要上锁
	m.mu.Lock()	
	read, _ = m.read.Load().(readOnly)
	//如果entry被标记expunge,则表明dirty没有key,可添加入dirty,并更新entry
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {  //确保未被标记成删除,即e 指向的是非 nil 的
			//标记成未被删除,往dirty中添加数据
			m.dirty[key] = e
		}
		//更新
		e.storeLocked(&value)
	//dirty中存在这个k,更新数据
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
	//dirty中无新的数据,往dirty中加入一个新key
		if !read.amended {
	//从read中复制数据	
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
	//将entry加入到dirty中
		m.dirty[key] = newEntry(value)
	}
	//解锁
	m.mu.Unlock()
}

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
        }
    }
}

特点:
1、unexpungeLocked 函数确保了 entry 没有被标记成已被清除。

2、如果 entry 先前被清除过了,那么在 mutex 解锁之前,它一定要被加入到dirty map中

3、如果 entry 的 unexpungeLocked 返回为 true,那么就说明 entry 之前被标记成了
   expunged,并经过 CAS 操作成功把它置为 nil。

4、将read中未删除的数据加入到dirty中


## Delete函数:删除一个entry
func (m *Map) Delete(key interface{}) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	//从dirty中删除数据,直接删除
	//如果read中没有,并且dirty中有新元素,那么就去dirty中去找
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		//double-checking
		if !ok && read.amended {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
	//从read删除数据,打标记
	//如果read中存在该key,则将该value 赋值nil(采用标记的方式删除!)
	//此时使用的是CAS,不需要加锁
	if ok {
		e.delete()
	}
}

从read删除数据的情况:
func (e *entry) delete() (hadValue bool) {
	for {
		p := atomic.LoadPointer(&e.p)
		//检测是否已经标记过
		if p == nil || p == expunged {
			return false
		}
		//原子操作,e.p标记为nil
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return true
		}
	}
}

Range函数:遍历
func (m *Map) Range(f func(key, value interface{}) bool) {
	read, _ := m.read.Load().(readOnly)
	//如果m.dirty中有新数据,先升级dirty后再遍历
	if read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		//double-checking双重检测
		if read.amended {      //dirty中有新数据,从dirty中更新到read字典来
			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
		}
	}
}

总结:

1、 sync.Map的核心思想是用空间换时间,用两个map来存储数据(read和dirty),
    read支持原子操作,操作一般不加锁,可以看作是dirty 的cache;

2、 sync.Map的4种操作

Load:先到read字典中读取,如果有则直接返回结果,如果没有或者是被删除,
      则到dirty加锁中读取,如果有则返回结果,无论是否存在均更新miss

一个重要的点:将dirty提升为read的过程是先将dirty的元素加载到read中,然后将
              dirty置空,计数清0,且搬的时候会看key在read是不是标记空的,是
              就不需要搬过去了

Store:
     更新:先到read中看看有没有,如果有且没被标记删除直接更新key,然后返回
          否则上锁更新dirty中的key,注意:两个map的key的value实际指向的是同一份
     增加key:直接增加到dirty中,同时标记read的map是脏的
     
Delete:先到read中看看有没有,如果有则直接更新为nil,如果没有,且read标记
        为脏则到dirty中直接加锁删除,如果read没有且read不脏,那证明dirty中
        肯定也没有,不需要去看。
        还有dirty提升为read时也进行物理删除,此时会判断read中标记删除,
        那么dirty中对应的键不需要拷贝过去,实现真正的删除
        这些都是延迟删除的优化做法!
        
Range:遍历sync.Map,可能涉及提升操作,当read的脏字段是true,则需要提升,
       然后遍历提升后的新map 

注意:线程安全的 Map 在第一次使用之后,不允许被拷贝。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值