sync.map源码分析

sync.map源码阅读分享

概述

最近在项目中遇到过对线程不安全的map并发写的错误。小菜鸟之前是写java的,知道jdk中有提供的线程安全的map,于是乎在想golang中是不是也有呢?搜索了一下还真有,sync.map。然后阅读了一下其底层实现源码,在此整理了一下自己的理解。希望大佬们能批评质疑指正,也希望能对像我这样的go初级玩家有一点点帮助。sync.map采用的是用空间换时间的策略。其维护了read和dirty两份数据,如图所示 (图片转自https://www.jianshu.com/p/ec51dac3c65b)
图片
数据结构如下:

type Map struct {
	mu Mutex //有时对底层数据进行读写操作时需要加锁
	read atomic.Value // 提供服务的主要数据结构
	dirty map[interface{}]*entry 
    misses int // 从read中miss数据的次数,当达到某个阈值时将dirty替换为read
}

其中readOnly数据如下:

type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}

amend字段用来表示read和dirty之间的关系(dirty是否存在read中不存在的key).
接下来将介绍以下问题:

  1. 通过什么方式来表达两份数据之间的关系?
  2. 两份数据是怎么提供服务的,以及两份数据的相互转换过程。
  3. 一些常用的操作

两份数据之间的关系怎么表达

如上图所示,两份数据最终共享同一份value值。但是两份数据中key的数据未必一致,可能有些key在read中存在,而在dirty中没有出现,有些key在dirty中有出现,在read中却没有出现。
一方面,可以通过直接读取对应hash得到数据kv值是否存在;
另一方面,有些字段来表示两份数据之间的关系,amend表示dirty中存在read中没有的key,对于read对应map的 *entry值,nil表示该key-value已被逻辑删除,并且dirty中也有该key值,expunged则表示dirty中没有该key值。

两份数据转换的时机

  • dirty到read
    读、写、删除等操作会优先操作read,此时不需要加锁效率比较高,如果read没有相应数据才去dirty找。在读的过程中会更新misses的值来记录在read中读取数据失败的次数,当misses达到一定阈值之后会触发将dirty的数据交给read; 此外,当进行range操作时,如果通过amend发现read的数据不全,此时会把dirty数据交给read.
  • read数据复制到dirty
    在进行写操作时,如果发现dirty为nil,此时会把read中未被删除的数据复制给dirty。

一些常用的操作

主要介绍下对map的读、写、删除操作。

读操作

读操作对应的方法为Load方法:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

首先去read中判断key是否存在,这一步不需要加锁。如果存在,根据是否已被删除进行后续的处理。否则,加锁再次判断read中是否存在这个key。

这样做的目的是应对这样的场景: 第一次判断之后,发生了dirty到read的转换,即read接管dirty的数据.此后dirty为nil,如果不二次判断,会出现map中实际有该数据却不能正常返回的情况。

如果read中没有该key值则去dirty中查找相应数据。

写操作

func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			m.dirty[key] = e
		}
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
		if !read.amended {
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}
  • 依然是先看read中是否存在该数据,如果存在并且未被删除(此时dirty中也必有该数据),因为read和dirty共享同一份value数据,所以直接更新相应的value值即可。
  • 然后类似load的过程加锁二次判断,如果对应value为expunge,此时说明dirty中没有这个key,此时要在dirty中加入这个数据并更新其value值,同样因为read,dirty共享同一份value,所以这次更新对于read同样有效。
  • 如果read和dirty中没有该数据,此时判断是否需要把read的值复制至dirty并新增数据

删除操作

func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
	if ok {
		e.delete()
	}
}

如果read中存在该数据则直接通过read将entry值置为nil(实际上read和dirty共享entry),否则删除dirty中相应的值。

一些问题

  1. dirty和read数据的区别?
    dirty通常情况下会包含全部的 有效 数据(把数据交给read之后除外)
  2. read中为什么可能存在dirty中不存在的key?
    主要存在两个这样的场景:
    - dirty把数据交给read之后,此时自身数据为空
    - 将read数据复制给dirty时,已被删除的数据不会复制到dirty,此时read存在dirty中不存在的key(已删除)。
  3. 删除数据时为什么不物理删除,并且存在nil和expunge两种状态?
    sync.map是对map的封装,map底层存储结构是数组链表,每个节点是一块连续内存空间含有8个k-v对,物理删除涉及到元素移动,节点内存块释放等操作,比较复杂,影响效率。 nil,expunge用来表达这个元素在这两份数据中的状态,nil表示dirty中有该key值,此时直接更新entry即可。expunge表明dirty中没有该key值,此时要增加该key值并更新entry. 这块个人感觉还是有点空间换时间的味道。

总结

目的:是读写分离提高效率。
技术手段:空间换时间
核心:两份数据的维护

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值