首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >[go源码] sync/map.go 源码

[go源码] sync/map.go 源码

原创
作者头像
Librant
修改2025-05-15 13:43:50
修改2025-05-15 13:43:50
1820
举报
文章被收录于专栏:跟我一起学 K8s跟我一起学 K8s

1 代码路径

golang源码:https://github.com/golang/go

commit c62099cfac6b0fd46efbdab7205bb17597096472

这里使用最新的 master 代码进行学习,与时俱进;

提前安装好 go 的开发环境,下载好 go 的源码,如果这些都不会的话,这边建议先不着急学习源码;

go version go1.24.3 windows/amd64

开发环境:VSCODE,方便代码跳转;

废话不多说,直接上干货,我知道程序员都是比较性急的生物;

2 代码走读

本节学习的代码路径:src\sync\map.go

代码语言:txt
复制
E:\gowork\src\github.com\golang\go\src\sync\map.go

sync 包是 Go 标准库中的一个基础并发原语包,提供了多种用于并发控制的结构和原语,比如互斥锁(Mutex)、等待组(WaitGroup)、一次性执行(Once)、条件变量(Cond)等。

2.1 src\sync\map.go

代码语言:go
复制
type Map struct {
    // go vet 工具会检查当前的结构体实例是否被复制,编译器会忽略该字段
    _ noCopy
    
    mu Mutex
    
    read atomic.Pointer[readOnly]
    dirty map[any]*entry
    misses int
}

1)Map

与 map[any]any,sync 包下的 Map 是并发安全的,无需额外的加锁和解锁操作,实际 Map 结构体中是自带自旋锁的;

下面对每个字段进行详细的介绍:

  • mu Mutex:自旋锁
  • read atomic.Pointer[readOnly]
  • dirty map[any]*entry
  • misses int

read 字段:包含 map 的部分内容,主要用于加速读取性能;典型的用空间换时间的做法;

atomic.Pointer[any]:src\sync\atomic\type.go 下的

type Pointer[T any] struct {} 结构体,当前只需要了解,这个 Pointer 结构体提供原子操作 T 类型的值的方法,后续会在 atomic 源码进行详细的讲解;

也就是 read 是一个原子读取 readOnly 类型的变量;那么 readOnly 又长啥样呢?接着往下看:

代码语言:txt
复制
type readOnly struct {
	m       map[any]*entry
	amended bool // true if the dirty map contains some key not in m.
}

在 readOnly 结构体中,只有两个字段:

  • m map[any]*entry:保存了 Map 中 dirty 中的部分 key-value 值,可以看成 dirty 的缓存;
    • 1、这个值时是在什么时候进行更新呢?
  • amended bool:当 Map 结构体中的 dirty 包含 m 中没有的 key 的值时为 true,反过来理解就是,当 amended 为 false 的意思就是 m 和 dirty 中保存的值时一致的;

dirty 字段:包含了 map 中需要加锁才能访问的内容,当 read map 的命中率变低时,会将 dirty 整体提升为 read map,提高读取的性能;

misses 字段: 这个字段比较好理解,读取的时候,没有命中 read 的次数,也就是取 dirty 中读取的次数;

在 entry 结构体中:

代码语言:go
复制
type entry struct {
    p atomic.Pointer[any]
}

p 指向该条目(entry)中存储的 interface{} 类型的值。

代码语言:go
复制
func newEntry(i any) *entry {
	e := &entry{}
	e.p.Store(&i)
	return e
}

func newEntry() 函数提供创建 entry 实例,原子存储 value;这个原子存储的功能是 atomic.Pointer 结构体 Store() 方法提供的能力;

entry 是 Map 内部定义的结构体,是保存 Map 中的 value 的地方,entry 提供的方法列表:

代码语言:go
复制
func (e *entry) load() (value any, ok bool)
func (e *entry) tryCompareAndSwap(old, new any) bool
func (e *entry) unexpungeLocked() (wasExpunged bool)
func (e *entry) swapLocked(i *any) *any
func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool)
func (e *entry) delete() (value any, ok bool)
func (e *entry) trySwap(i *any) (*any, bool)
func (e *entry) tryExpungeLocked() (isExpunged bool)

Map 对外提供的方法列表:

代码语言:go
复制
func (m *Map) Load(key any) (value any, ok bool)
func (m *Map) Store(key, value any)
func (m *Map) Clear()
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
func (m *Map) Delete(key any)
func (m *Map) Swap(key, value any) (previous any, loaded bool)
func (m *Map) CompareAndSwap(key, old, new any) (swapped bool)
func (m *Map) CompareAndDelete(key, old any) (deleted bool)
func (m *Map) Range(f func(key, value any) bool)

Map 不对外提供的方法列表:

代码语言:go
复制
func (m *Map) loadReadOnly() readOnly
func (m *Map) missLocked()
func (m *Map) dirtyLocked()

2)var expunged = new(any)

expunged 是一个特殊的指针,用于标记那些已经从 dirty map 中被删除的条目。

在 sync.Map 中,删除一个 key 不是立即从 map 中进行删除,是先将 entry 标记成哨兵指针(sentinel pointer);

3)Map 提供的方法

代码语言:go
复制
func (m *Map) loadReadOnly() readOnly {
	if p := m.read.Load(); p != nil {
		return *p
	}
	return readOnly{}
}

loadReadOnly():不可导出方法,提供原子读取 Map 中的 read 字段的值;

代码语言:go
复制
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	m.read.Store(&readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

missLocked():方法提供对 misses 计数,对于没有命中 read 的次数进行计数

当 misses 的值比 dirty 的长度相等或大于的时候,会重新将 dirty 提升到 read 中,提高读取的性能;

然后将 m.dirty 的值置为 nil;

  • 这里之所以置为 nil,是因为 read 已经是 dirty 的全量信息了,没有必要再保存一份;其次是因为原来的 dirty 存储的内存,可以被 gc 进行回收,还有一个原因是,在下一次加锁写 dirty 的时候,需要对值为 expunged 的 key 真正从 map 的 bucket 中进行删除;

那么接下来看下 dirtyLocked() 方法:

代码语言:go
复制
func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}

	read := m.loadReadOnly()
	m.dirty = make(map[any]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

dirtyLocked():如果 m.dirty 为空,根据 read.m 的长度创建 m.dirty,实际 dirty 的长度可能比 read.m 小,毕竟有部分已经删除的 key 不会再保存到 m.dirty 中;

那我们继续看下 tryExpungeLocked() 方法:

代码语言:go
复制
func (e *entry) tryExpungeLocked() (isExpunged bool) {
	p := e.p.Load()
	for p == nil {
		if e.p.CompareAndSwap(nil, expunged) {
			return true
		}
		p = e.p.Load()
	}
	return p == expunged
}

tryExpungeLocked():可以看到,首先读取 p 的值,如果 p 的值为空,就会将 expunged 哨兵指针赋值给 p,并返回 true;这个函数会判断 key 对应的 value 是否是 expunged,如果为 nil,则设置成 expunged,非空则返回 false;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 代码路径
  • 2 代码走读
    • 2.1 src\sync\map.go
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档