sync.Map原理浅析

一、概述

sync.Map 常用于并发编程场景,通过原子操作保证读操作的线程安全性,同时利用不可变结构避免数据竞争。

  • 写操作:直接写入;
  • 读操作:先读 read 区,没有再读取 dirty 区;
graph TD A[写操作] --> B[dirty区] C[读操作] --> D[read区] --> E{get?} E -->|no|F[dirty区] E -->|yes|G[return] F --> G

二、核心数据结构

golang 复制代码
type Map struct {
    mu     Mutex
    read   atomic.Pointer[readOnly]
    dirty  map[any]*entry
    misses int
}
  • mu加锁作用。 一个 sync.Mutex 类型,保护后文的 dirty 字段。
  • read只读的数据。 这是一个 atomic.Pointer 类型,实际存储的是 readOnly 结构体。read 中的键值对可以无锁访问,适合读多写少的场景,能够显著提升读取性能。
  • dirty包含最新写入的数据。 它是一个普通的 map,存储的键值对可能包含 read 中没有的部分。当对 sync.Map 进行写操作(如插入、更新、删除)时,通常会先在 dirty 中进行操作。
  • misses计数作用。该计数器用于记录在 read 中查找键未命中的次数。当 misses 达到一定阈值时,会触发 dirty 提升为 read 的操作。
golang 复制代码
type readOnly struct {
    m       map[any]*entry
    amended bool 
}
  • m:它是一个普通的 map。
  • amended:一个 bool 类型的标志字段,Map.dirty 的数据和这里的 m 中的数据不一样的时候,为true。
golang 复制代码
type entry struct {
    p atomic.Pointer[any]
}
  • p:一个指针类型,指向实际存储的数据。

三、核心操作原理

sync.Map 是 Go 语言标准库中提供的一种并发安全的 map 类型。与普通的 map 不同,sync.Map 针对并发操作进行了优化,提供了一些原子操作来保证线程安全。其主要原理涉及到一些锁的优化和内部数据结构的设计。

3.1 数据读取流程

  1. 首先尝试从 read 中查找指定的键。如果找到,并且键对应的 entry 不为 nil,则直接返回该值。
  2. 如果在 read 中未找到,并且 read 的 amended 字段为 true,说明 dirty 中可能存在该键。此时会加锁,再次检查 read,若仍未找到,就从 dirty 中查找。
  3. 每次从 dirty 中查找后,会调用 missLocked 方法增加 misses 计数器的值。当 misses 达到 len(dirty) 时,会将 dirty 提升为 read,并清空 dirty。

3.2 数据删除流程

  1. 先尝试从 read 中查找该键。如果找到,将对应的 entry 标记为已删除。
  2. 如果在 read 中未找到,并且 read 的 amended 字段为 true,会加锁从 dirty 中删除该键。

3.3 数据增(改)流程

  1. 先尝试在 read 中查找该键,如果找到且 entry 未被标记为已删除,直接更新其值。
  2. 如果在 read 中未找到,或者 entry 已被标记为已删除,会加锁进行后续操作。
  3. 加锁后:
    1. 再次检查 read。若在 read 中找到且 entry 标记为已删除,将其加入 dirty 并更新值;
    2. 若在 dirty 中找到,直接更新值;
    3. 若都未找到,且 dirty 还未初始化,会将 read 中的所有键值对复制到 dirty,然后将新的键值对插入 dirty。

四、使用技术技巧

  1. 读写分离设计: 借助 read 和 dirty 两个映射来降低锁的使用频率,提升并发性能。
  2. 原子操作和 CAS(CompareAndSwap): sync.Map 使用了原子操作和 CAS 操作来执行读写操作,避免了整个map上的锁。这意味着在大多数情况下,读操作可以并发执行,而写操作只会锁定所在的那个段。
  3. 延迟删除 (Deferred Deletion): sync.Map 支持延迟删除(Lazy Deletion),即当一个 key 被删除时,并不会立即从底层的数据结构中移除,而是通过标记删除的方式,等待下一次 GC 执行删除。这样可以减小删除操作对并发读取的影响。
  4. 无锁访问: 在大多数情况下,sync.Map 会进行无锁访问,即读操作不需要锁。只有在写操作时,对应的段会被锁定,以确保写操作的原子性。

五、总结

  • 优点
    • Go官方所出,通过读写分离,降低锁时间来提高效率。
  • 缺点
    • 不适用于大量写的场景,这样会导致 read map 读不到数据而进一步加锁读取,同时 dirty map 也会一直晋升为 read map,整体性能较差,甚至没有单纯的 map + metux 高。
  • 适用场景
    • 适用于读多写少的场景。
  • 发散思考
    • 如果要实现一个类似的 Map,核心点就是把锁的粒度尽可能降低来提高运行速度。
    • 对一个大 map 进行 hash,其内部是 n 个小 map,根据 key 来 hash 确定在具体的那个小 map 中,这样加锁的粒度就变成 1/n 了。
相关推荐
n8n3 小时前
Go语言GC调优全面指南
go
n8n3 小时前
Go 协程在实际项目中的应用详解
go
苏琢玉4 小时前
再也不用翻一堆日志!一键部署轻量级错误监控系统,帮你统一管理 PHP 报错
go·github·php
程序员爱钓鱼6 小时前
Go语言实战案例——进阶与部署篇:使用Docker部署Go服务
后端·google·go
程序员爱钓鱼19 小时前
Go语言实战案例——进阶与部署篇:编写Makefile自动构建Go项目
后端·算法·go
该用户已不存在19 小时前
别再用 if err != nil 了,学会这几个技巧,假装自己是Go大神
后端·go
n8n21 小时前
Go语言操作Redis全面指南
go
王中阳Go1 天前
为什么很多公司都开始使用Go语言了?为啥这个话题这么炸裂?
java·后端·go
Sesame22 天前
gotun: 一个基于SSH协议的零配置HTTP代理工具
go
豆浆Whisky3 天前
Go泛型实战指南:从入门到工程最佳实践|Go语言进阶(12)
后端·go