我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Golangsync.Map原理深入分析讲解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Golangsync.Map原理深入分析讲解

GO语言内置的map

go语言内置一个map数据结构,使用起来非常方便,但是它仅支持并发的读,不支持并发的写,比如下面的代码:

在main函数中开启两个协程同时对m进行并发读和并发写,程序运行之后会报错:

package main
func main()  {
	m := make(map[int]int)
	go func()  {
		for {
			_ = m[1]
		}
	}()
	go func()  {
		for {
			m[2] = 2
		}
	}()
	select {}
}

改进

既然不可以并发的写,我们可以给map加一个读写锁,这样就不会有并发写冲突的问题了:

import "sync"
func main() {
	m := make(map[int]int)
	var lock sync.RWMutex
	go func() {
		for {
			lock.RLock()
			_ = m[1]
			lock.RUnlock()
		}
	}()
	go func() {
		for {
			lock.Lock()
			m[2] = 2
			lock.Unlock()
		}
	}()
	select {}
}

这种方式的实现非常简洁,但也存在一些问题,比如在map的数据非常大的情况下,一把锁会导致大并发的客户端共争一把锁。

sync.Map

sync.Map是官方在sync包中提供的一种并发map,使用起来非常简单,和普通map相比,只有遍历的方式有区别:

package main
import (
	"fmt"
	"sync"
)
func main() {
	var m sync.Map
	// 1. 写入
	m.Store("apple", 1)
	m.Store("banana", 2)
	// 2. 读取
	price, _ := m.Load("apple")
	fmt.Println(price.(int))
	// 3. 遍历
	m.Range(func(key, value interface{}) bool {
		fruit := key.(string)
		price := value.(int)
		fmt.Println(fruit, price)
		return true
	})
	// 4. 删除
	m.Delete("apple")
	// 5. 读取或写入
	m.LoadOrStore("peach", 3)
}

sync.Map是通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上。

读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty。

读取 read 并不需要加锁,而读或写 dirty 都需要加锁,另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上,对于删除数据则直接通过标记来延迟删除。

在map + 锁的基础上,它有着几个优化点:

  • 空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
  • 使用只读数据(read),避免读写冲突。
  • 动态调整,miss次数多了之后,将dirty数据提升为read。
  • double-checking。
  • 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
  • 优先从read读取、更新、删除,因为对read的读取不需要锁。

sync.Map原理分析

sync.Map的结构

sync.Map的实现在class="lazy" data-src/sync/map.go中,首先来看Map结构体:

type Map struct {
    // 当涉及到脏数据(dirty)操作时候,需要使用这个锁
    mu Mutex
    // read是一个只读数据结构,包含一个map结构,
    // 读不需要加锁,只需要通过 atomic 加载最新的指正即可
    read atomic.Value // readOnly
    // dirty 包含部分map的键值对,如果操作需要mutex获取锁
    // 最后dirty中的元素会被全部提升到read里的map去
    dirty map[interface{}]*entry
    // misses是一个计数器,用于记录read中没有的数据而在dirty中有的数据的数量。
    // 也就是说如果read不包含这个数据,会从dirty中读取,并misses+1
    // 当misses的数量等于dirty的长度,就会将dirty中的数据迁移到read中
    misses int
}

上述结构体中的read字段实际上是一个包含map的结构体,该结构体中的map是一个read map,对该map的访问不需要加锁,但是增加的元素不会被添加到这个map中,元素会被先增加到dirty中,后续才会被迁移到read只读map中。

readOnly结构体中还有一个amended字段,该字段是一个标志位,用来表示read map中的数据是否完整。假设当前要查找一个key,会先去read map中找,如果没有找到,会判断amended是否为true,如果为true,说明read map的数据不完整,需要去dirty map中找。

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
    // m包含所有只读数据,不会进行任何的数据增加和删除操作 
    // 但是可以修改entry的指针因为这个不会导致map的元素移动
    m       map[interface{}]*entry
    // 标志位,如果为true则表明当前read只读map的数据不完整,dirty map中包含部分数据
    amended bool // true if the dirty map contains some key not in m.
}

entry

readOnly.mMap.dirty存储的值类型是*entry,它包含一个指针p, 指向用户存储的value值,结构如下:

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

其中p对应着三种值:

  • p == nil: 键值已经被删除,且 m.dirty == nil,这个时候dirty在等待read的同步数据。
  • p == expunged: 键值已经被删除,但 m.dirty!=nil 且 m.dirty 不存在该键值(dirty已经得到了read的数据同步,原来为nil的值已经被标记为了expunged没有被同步过来)。
  • 除以上情况,则键值对存在,存在于 m.read.m 中,如果 m.dirty!=nil 则也存在于 m.dirty

下面是sync.Map的结构示意图:

查找

查找元素会调用Load函数,该函数的执行流程:

  • 首先去read map中找值,不用加锁,找到了直接返回结果。
  • 如果没有找到就判断read.amended字段是否为true,true说明dirty中有新数据,应该去dirty中查找,开始加锁。
  • 加完锁以后又去read map中查找,因为在加锁的过程中,m.dirty可能被提升为m.read。
  • 如果二次检查没有找到key,就去m.dirty中寻找,然后将misses计数加一。
// class="lazy" data-src/sync/map.go
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 首先从只读ready的map中查找,这时不需要加锁
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    // 如果没有找到,并且read.amended为true,说明dirty中有新数据,从dirty中查找,开始加锁了
    if !ok && read.amended {
        m.mu.Lock() // 加锁
       // 又在 readonly 中检查一遍,因为在加锁的时候 dirty 的数据可能已经迁移到了read中
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        // read 还没有找到,并且dirty中有数据
        if !ok && read.amended {
            e, ok = m.dirty[key] //从 dirty 中查找数据
            // 不管m.dirty中存不存在,都将misses + 1
            // missLocked() 中满足条件后就会把m.dirty中数据迁移到m.read中
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}

misses计数

misses计数是有上限的,如果misses次数达到m.dirty的长度,就开始迁移数据,程序会直接将m.dirty提升为m.read,然后将m.dirty置为nil,等到下次插入新数据的时候,程序才会把read map中的值全部复制给dirty map。

// class="lazy" data-src/sync/map.go
func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {//misses次数小于 dirty的长度,就不迁移数据,直接返回
        return
    }
    m.read.Store(readOnly{m: m.dirty}) //开始迁移数据
    m.dirty = nil   //迁移完dirty就赋值为nil
    m.misses = 0  //迁移完 misses归0
}

新增和更新

新增或者更新元素会调用Store函数,该函数的前面几个步骤与Load函数是一样的:

  • 首先去read map中找值,不用加锁,找到了直接调用tryStore()函数更新值即可。
  • 如果没有找到就开始对dirty map加锁,加完锁之后再次去read map中找值,如果存在就判断该key对应的entry有没有被标记为unexpunge,如果没有被标记,就直接调用storeLocked()函数更新值即可。
  • 如果在read map中进行二次检查还是没有找到key,就去dirty map中找,找到了直接调用storeLocked()函数更新值。
  • 如果dirty map中也没有这个key,说明是新加入的key,首先要将read.amended标记为true,然后将read map中未删除的值复制到dirty中,最后向dirty map中加入这个值。
// class="lazy" data-src/sync/map.go
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
   // 直接在read中查找值,找到了,就尝试 tryStore() 更新值
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    // m.read 中不存在
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() { // 未被标记成删除,前面讲到entry数据结构时,里面的p值有3种。1.nil 2.expunged,这个值含义有点复杂,可以看看前面entry数据结构 3.正常值
            m.dirty[key] = e // 加入到dirty里
        }
        e.storeLocked(&value) // 更新值
    } else if e, ok := m.dirty[key]; ok { // 存在于 dirty 中,直接更新
        e.storeLocked(&value)
    } else { // 新的值
        if !read.amended { // m.dirty 中没有新数据,增加到 m.dirty 中
            // We're adding the first new key to the dirty map.
            // Make sure it is allocated and mark the read-only map as incomplete.
            m.dirtyLocked() // 从 m.read中复制未删除的数据
            m.read.Store(readOnly{m: read.m, amended: true}) 
        }
        m.dirty[key] = newEntry(value) //将这个entry加入到m.dirty中
    }
    m.mu.Unlock()
}

在Store函数中我们用到了两个用于更新值的函数:tryStore以及storeLockedtryStore函数是先判断p有没有被标记为expunged(软删除),如果被标记了就直接返回false,如果没有被标记,就将p指向的值进行更新然后返回true。

storeLocked函数是直接将p指向的值进行更新。

// tryStore stores a value if the entry has not been expunged.
//
// If the entry is expunged, tryStore returns false and leaves the entry
// unchanged.
func (e *entry) tryStore(i *interface{}) bool {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == expunged {
			return false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
	}
}
// storeLocked unconditionally stores a value to the entry.
//
// The entry must be known not to be expunged.
func (e *entry) storeLocked(i *interface{}) {
	atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

将read map中的值复制到dirty map中:

m.dirtyLocked()函数用于将read map中的值复制到dirty map中:

func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}
	read, _ := m.read.Load().(readOnly)
	m.dirty = make(map[interface{}]*entry, len(read.m))
	for k, e := range read.m {
		// 判断值是否被删除,被标记为expunged的值不会被复制到read map中
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}
// expunged实际上是一个指向空接口的unsafe指针
var expunged = unsafe.Pointer(new(interface{}))
func (e *entry) tryExpungeLocked() (isExpunged bool) {
	p := atomic.LoadPointer(&e.p)
	// 如果p为nil,就会被标记为expunged
	for p == nil {
		if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
			return true
		}
		p = atomic.LoadPointer(&e.p)
	}
	return p == expunged
}

下面是对sync.Map进行读写操作的示意图,正常读写且read map中有数据,程序只会访问read map,而不会去加锁:

删除

删除会调用Delete函数,该函数的步骤如下:

  • 首先去read map中找key,找到了就调用e.delete()函数删除。
  • 如果在read map中没有找到值就开始对dirty map加锁,加锁完毕之后再次去read map中查找,找到了就调用e.delete()函数删除。
  • 如果二次检查都没有找到key(说明这个key是被追加之后,还没有提升到read map中就要被删除),就去dirty map中删除这个key。
// class="lazy" data-src/sync/map.go
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
    // 从 m.read 中开始查找
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended { // m.read中没有找到,并且可能存在于m.dirty中,加锁查找
        m.mu.Lock() // 加锁
        read, _ = m.read.Load().(readOnly) // 再在m.read中查找一次
        e, ok = read.m[key]
        if !ok && read.amended { //m.read中又没找到,amended标志位true,说明在m.dirty中
            delete(m.dirty, key) // 删除
        }
        m.mu.Unlock()
    }
    if ok { // 在 m.read 中就直接删除
        e.delete()
    }
}
func (e *entry) delete() (hadValue bool) {
	for {
		p := atomic.LoadPointer(&e.p)
		// 已标记为删除
		if p == nil || p == expunged {
			return false
		}
		// 原子操作,e.p标记为nil,GO的GC会将对象自动删除
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return true
		}
	}
}

key究竟什么时候会被删除

我们可以发现,如果read map中存在待删除的key时,程序并不会去直接删除这个key,而是将这个key对应的p指针指向nil。

在下一次read -> dirty的同步时,指向nil的p指针会被标记为expunged,程序不会将被标记为expunged的 key 同步过去。

等到再一次dirty -> read同步的时候,read会被dirty直接覆盖,这个时候被标记为expunged的key才真正被删除了,这就是sync.Map的延迟删除。

到此这篇关于Golang sync.Map原理深入分析讲解的文章就介绍到这了,更多相关Golang sync.Map内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Golangsync.Map原理深入分析讲解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Golangsync.Map原理深入分析讲解

go中map数据结构不是线程安全的,即多个goroutine同时操作一个map,则会报错,因此go1.9之后诞生了sync.Map,sync.Map思路来自java的ConcurrentHashMap
2022-12-17

ReactHooks核心原理深入分析讲解

这篇文章主要介绍了reacthooks实现原理,文中给大家介绍了useStatedispatch函数如何与其使用的FunctionComponent进行绑定,节后实例代码给大家介绍的非常详细,需要的朋友可以参考下
2022-12-17

MySQL索引设计原则深入分析讲解

为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引。索引设计不合理或者缺少索引都会对数据库和应用程序的性能造成障碍。高效的索引对于获得良好的性能非常重要。设计索引时,应该考虑相应准则
2023-01-02

ReactFiber原理深入分析

Fiber可以理解为一个执行单元,每次执行完一个执行单元,ReactFiber就会检查还剩多少时间,如果没有时间则将控制权让出去,然后由浏览器执行渲染操作,这篇文章主要介绍了ReactFiber架构原理剖析,需要的朋友可以参考下
2023-01-10

Vuecomputed实现原理深入讲解

computed又被称作计算属性,用于动态的根据某个值或某些值的变化,来产生对应的变化,computed具有缓存性,当无关值变化时,不会引起computed声明值的变化。产生一个新的变量并挂载到vue实例上去
2022-11-13

Vuewatch原理源码层深入讲解

watch是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和computed进行比较。那么本文就来带大家从源码理解watch的工作流程,以及依赖收集和深度监听的实现
2022-11-13

GoComparableType原理深入解析

这篇文章主要为大家介绍了GoComparableType原理深入解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-06

深入理解Oracle锁表原因分析

深入理解Oracle锁表原因分析,需要具体代码示例随着企业数据库规模的不断增长和复杂性的加深,数据库锁表问题逐渐成为数据库管理员以及开发人员需要面对和解决的重要挑战之一。在Oracle数据库中,锁表是指当一个会话获取了对某个表或者行的锁之
深入理解Oracle锁表原因分析
2024-03-10

ReactContext原理深入理解源码示例分析

这篇文章主要为大家介绍了ReactContext原理深入理解源码示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-03

Android权限机制深入分析讲解

Android的权限管理遵循的是“最小特权原则”,即所有的Android应用程序都被赋予了最小权限。一个Android应用程序如果没有声明任何权限,就没有任何特权
2022-12-08

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录