一文带你深入探究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基于文件魔数判断文件类型的案例代码

    Golang基于文件魔数判断文件类型的案例代码

    这篇文章主要介绍了Golang基于文件魔数判断文件类型,本文介绍了基于文件魔数判断文件类型的方法,主要涉及如何ReadSeek读取文件指定字节内容,然后介绍文件魔数,最后给出示例基于魔数判断文件类型,需要的朋友可以参考下
    2023-02-02
  • Go 使用xorm操作mysql详情

    Go 使用xorm操作mysql详情

    这篇文章主要介绍了Go 使用xorm操作mysql详情,golang orm 库 xorm 的使用和项目结构。更多详细neural,需要的小伙伴可以参考下面文章内容
    2022-01-01
  • 浅谈go中切片比数组好用在哪

    浅谈go中切片比数组好用在哪

    数组和切片都是常见的数据结构,本文将介绍Go语言中数组和切片的基本概念,同时详细探讨切片的优势,感兴趣的可以了解下
    2023-06-06
  • 详解Go语言如何实现二叉树遍历

    详解Go语言如何实现二叉树遍历

    这篇文章主要为大家详解介绍了Go语言中如何实现二叉树遍历,文中的示例代码讲解详细,对我们学习Go语言有一定帮助,需要的可以参考一下
    2022-04-04
  • golang的基础语法和常用开发工具详解

    golang的基础语法和常用开发工具详解

    这篇文章主要介绍了golang的基础语法和常用开发工具,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • Go语言之使用pprof工具查找goroutine(协程)泄漏

    Go语言之使用pprof工具查找goroutine(协程)泄漏

    这篇文章主要介绍了Go语言之使用pprof工具查找goroutine(协程)泄漏,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • 利用Go语言追加内容到文件末尾

    利用Go语言追加内容到文件末尾

    关于Go语言读写文件,网上很多教程了,但是今天有个需求,想要把内容追加写到文件末尾,在网上找了很久才找到答案,现在分享给大家,有需要的可以参考借鉴。
    2016-09-09
  • go实现脚本解释器gscript

    go实现脚本解释器gscript

    这篇文章主要为大家介绍了go实现脚本解释器gscript示例代码,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • Go语言Zap日志库使用教程

    Go语言Zap日志库使用教程

    在项目开发中,经常需要把程序运行过程中各种信息记录下来,有了详细的日志有助于问题排查和功能优化;但如何选择和使用性能好功能强大的日志库,这个就需要我们从多角度考虑
    2023-02-02
  • 一文详解Go语言中Mutex互斥锁

    一文详解Go语言中Mutex互斥锁

    Golang中的Mutex互斥锁是一种常用的并发控制机制,用于保护共享资源的访问,在本文中,我们将深入探讨Mutex互斥锁的原理、日常使用、锁结构以及运行机制,需要的朋友可以参考下
    2023-12-12

最新评论