一文带你深入探究Go语言中的sync.Map

 更新时间:2023年04月27日 14:59:39   作者:金刀大菜牙  
在 Go 语言中,有一个非常实用的并发安全的 Map 实现:sync.Map,它是在 Go 1.9 版本中引入的。本文我们将深入探讨 sync.Map 的基本原理,帮助读者更好地理解并使用这个并发安全的 Map

在 Go 语言中,有一个非常实用的并发安全的 Map 实现:sync.Map,它是在 Go 1.9 版本中引入的。相比于标准库中的 map,它的最大特点就是可以在并发情况下安全地读写,而不需要加锁。在这篇博客中,我们将深入探讨 sync.Map 的基本原理,帮助读者更好地理解并使用这个并发安全的 Map。

1. Map 的基本实现原理

在介绍 sync.Map 的基本实现原理之前,我们需要先了解一下 Go 语言标准库中的 map 实现原理。在 Go 中,map 是基于哈希表实现的。当我们向 map 中添加元素时,它会根据 key 计算出一个哈希值,然后将这个值映射到一个桶中。如果该桶中已经有了元素,它会遍历桶中的元素,查找是否已经存在相同的 key,如果存在就更新对应的值,否则就添加一个新的键值对。

下面是一个简单的 map 示例:

m := make(map[string]int)
m["a"] = 1
m["b"] = 2
fmt.Println(m["a"]) // Output: 1

当我们运行这段代码时,Go 语言会自动帮我们分配一个哈希表和若干个桶,然后将键值对添加到对应的桶中。这样,当我们需要访问某个 key 对应的值时,Go 语言会根据哈希值快速定位到对应的桶,然后遍历桶中的元素,查找是否有相同的 key,如果找到了就返回对应的值。

2. sync.Map 的实现原理

sync.Map 是 Go 语言标准库中的一个并发安全的 Map 实现,它可以在并发情况下安全地读写,而不需要加锁。那么,它是如何实现这种并发安全性的呢?下面我们就来一步步地解析 sync.Map 的实现原理。

2.1 sync.Map 的结构体定义

首先,让我们来看一下 sync.Map 的结构体定义:

type Map struct {
    mu      sync.Mutex
    read    atomic.Value // readOnly
    dirty   map[interface{}]interface{}
    misses  int
    dirtyLocked uintptr
}

从上面的代码中可以看出,sync.Map 的实现主要是依赖于一个互斥锁(sync.Mutex)和两个 map(read 和 dirty)。其中,read 和 dirty 的作用分别是什么呢?我们先来看一下 read 的定义:

type readOnly struct {
    m       map[interface{}]interface{}
    amended bool
}

可以看到,read 只有一个成员 m,它是一个 map 类型。而 amended 则表示 read 中的键值对是否被修改过。接下来,我们来看一下 dirty 的定义:

type dirty struct {
    m       map[interface{}]interface{}
    dirty   map[interface{}]bool
    misses  int
}

和 read 不同的是,dirty 中包含了两个 map:m 和 dirty。其中,m 存储了被修改过的键值对,而 dirty 则存储了哪些键值对被修改过。

2.2 sync.Map 的读取实现

在 sync.Map 中,读取操作非常简单,直接从 readOnly 中的 m 中查找即可。如果 readOnly 中的键值对被修改过,则需要从 dirty 中查找。读取操作的实现代码如下:

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

在这段代码中,我们首先从 readOnly 中的 m 中查找键值对。如果键值对不存在且 readOnly 中的键值对被修改过,则需要获取互斥锁,并重新从 readOnly 中查找。如果还是没有找到,那么就从 dirty 中查找。

2.3 sync.Map 的写入实现

在 sync.Map 中,写入操作需要分两步完成。首先,我们需要判断 readOnly 中的键值对是否被修改过,如果没有被修改过,则直接将键值对添加到 readOnly 中的 m 中即可。否则,我们需要获取互斥锁,然后将键值对添加到 dirty 中的 m 中,并将对应的键添加到 dirty 中的 dirty 中。写入操作的实现代码如下:

func (m *Map) Store(key, value interface{}) {
    read, _ := m.read.Load().(readOnly)
    if v, ok := read.m[key]; !ok && !read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        if v, ok := read.m[key]; !ok {
            read = readOnly{m: read.m, amended: true}
        }
        read.m[key] = value
        m.read.Store(read)
        m.mu.Unlock()
    } else {
        m.mu.Lock()
        dirty := m.dirtyLocked != 0
        if !dirty {
            m.dirtyLocked = 1
            m.dirty = make(map[interface{}]interface{})
        }
        m.dirty[key] = value
        if !ok {
            m.dirty[key] = value
            m.dirty[key] = true
        }
        if dirty {
            m.mu.Unlock()
            return
        }
        m.read.Store(readOnly{m: read.m, amended: true})
        m.mu.Unlock()
    }
}

在这段代码中,我们首先从 readOnly 中的 m 中查找键值对。如果键值对不存在且 readOnly 中的键值对没有被修改过,则需要获取互斥锁,并重新从 readOnly 中查找。如果还是没有找到,则将键值对添加到 readOnly 中的 m 中,并将 amended 设置为 true。否则,我们需要获取互斥锁,并将键值对添加到 dirty 中的 m 中,并将对应的键添加到 dirty 中的 dirty 中。如果 dirty 中已经存在该键,则只需要更新 dirty 中的键值即可。如果 dirty 中没有该键,则需要在 dirty 中添加该键,并将该键的 dirty 置为 true。

接下来,我们需要判断 dirty 是否被锁定。如果 dirty 被锁定,则直接退出函数。否则,我们需要将 readOnly 中的 amended 设置为 true,并将 readOnly 存储回 read 中。

2.4 sync.Map 的删除实现

在 sync.Map 中,删除操作也需要分两步完成。首先,我们需要判断 readOnly 中的键值对是否被修改过,如果没有被修改过,则直接从 readOnly 中的 m 中删除键值对即可。否则,我们需要获取互斥锁,然后将键添加到 dirty 中的 dirty 中,并将 dirty 中的对应键的值设置为 false。删除操作的实现代码如下:

func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
    if _, ok := read.m[key]; ok || read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        if _, ok := read.m[key]; ok || read.amended {
            if m.dirty == nil {
                m.dirty = make(map[interface{}]interface{})
            }
            m.dirty[key] = false
            m.dirty[key] = true
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.mu.Unlock()
    }
}

在这段代码中,我们首先从 readOnly 中的 m 中查找键值对。如果键值对存在或者 readOnly 中的键值对被修改过,则需要获取互斥锁,并重新从 readOnly 中查找。如果还是没有找到,则将键添加到 dirty 中的 dirty 中,并将 dirty 中的对应键的值设置为 false。接下来,我们需要判断 dirty 是否为 nil,如果为 nil,则需要将 dirty 初始化为一个空 map。然后,我们将键添加到 dirty 中,并将 dirty 中的对应键的值设置为 true。最后,我们将 readOnly 中的 amended 设置为 true,并将 readOnly 存储回 read 中。

2.5 sync.Map 的遍历实现

在 sync.Map 中,遍历操作需要将 readOnly 和 dirty 中的所有键值对进行合并,并返回所有未被删除的键值对。遍历操作的实现代码如下:

func (m *Map) Range(f func(key, value interface{}) bool) {
    read, _ := m.read.Load().(readOnly)
    if read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        if read.amended {
            read = readOnly{
                m: merge(read.m, m.dirty),
            }
            read.amended = false
            m.read.Store(read)
            m.dirty = nil
        }
        m.mu.Unlock()
    }
    for k, v := range read.m {
        if !f(k, v) {
            break
        }
    }
}
​
func merge(m1, m2 map[interface{}]interface{}) map[interface{}]interface{} {
    if len(m1) == 0 && len(m2) == 0 {
        return nil
    }
    if len(m1) == 0 {
        return m2
    }
    if len(m2) == 0 {
        return m1
    }
    m := make(map[interface{}]interface{})
    for k, v := range m1 {
        m[k] = v
    }
    for k, v := range m2 {
        if _, ok := m[k]; !ok || !v.(bool) {
            m[k] = v
        }
    }
    return m
}

在这段代码中,我们首先从 readOnly 中获取所有的键值对,并检查是否有键值对被修改过。如果键值对被修改过,则需要获取互斥锁,并将 readOnly 和 dirty 中的键值对合并,然后将合并后的键值对存储回 readOnly 中,并将 dirty 设置为 nil。接下来,我们遍历 readOnly 中的所有键值对,并调用 f 函数来处理键值对。如果 f 函数返回 false,则遍历过程结束。

在这个 Range 函数中,我们还实现了一个名为 merge 的辅助函数,用于合并两个 map。在合并过程中,我们首先判断两个 map 是否为空,如果为空,则直接返回 nil。如果其中一个 map 为空,则返回另一个 map。否则,我们需要将 m1 中的键值对全部添加到新的 map 中,并逐个遍历 m2 中的键值对。如果 m2 中的键不存在于新的 map 中,或者 m2 中的键被删除,则将其添加到新的 map 中。

以上就是一文带你深入探究Go语言中的sync.Map的详细内容,更多关于Go语言sync.Map的资料请关注脚本之家其它相关文章!

相关文章

  • golang分层测试之http接口测试入门教程

    golang分层测试之http接口测试入门教程

    这篇文章主要介绍了golang分层测试之http接口测试入门教程,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • Golang中的关键字(defer、:=、go func())详细解读

    Golang中的关键字(defer、:=、go func())详细解读

    这篇文章主要介绍了Golang中的关键字(defer、:=、go func())详细解读,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-04-04
  • golang根据URL获取文件名的示例代码

    golang根据URL获取文件名的示例代码

    这篇文章主要为大家详细介绍了golang根据URL获取文件名,文中的示例代码讲解详细,对大家的学习或工作有一定的帮助,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-01-01
  • go语言实现文件分割的方法

    go语言实现文件分割的方法

    这篇文章主要介绍了go语言实现文件分割的方法,实例分析了Go语言操作文件的技巧,需要的朋友可以参考下
    2015-03-03
  • 一文详解go的defer和return的执行顺序

    一文详解go的defer和return的执行顺序

    go的defer和return是golang中的两个关键字,return用于返回函数的返回值,也可以参与一定的流程控制,defer是golang中的延迟调用,经常用于文件流的关闭,锁的解锁操作,本文给大家介绍了go的defer和return的执行顺序,需要的朋友可以参考下
    2024-07-07
  • Go语言的互斥锁的详细使用

    Go语言的互斥锁的详细使用

    本文主要介绍了Go语言的互斥锁的详细使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • go语言中strings包的用法汇总

    go语言中strings包的用法汇总

    Golang语言 strings标准库包主要涉及字符串的基本操作,下面我们来详细分析下吧
    2018-10-10
  • Golang学习笔记之延迟函数(defer)的使用小结

    Golang学习笔记之延迟函数(defer)的使用小结

    这篇文章主要介绍了Golang学习笔记之延迟函数(defer),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • go格式“占位符”输入输出 类似python的input

    go格式“占位符”输入输出 类似python的input

    这篇文章主要介绍了go格式“占位符”, 输入输出,类似python的input,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-04-04
  • golang 对私有函数进行单元测试的实例

    golang 对私有函数进行单元测试的实例

    这篇文章主要介绍了golang 对私有函数进行单元测试的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05

最新评论