【Go并发编程】sync.map源码阅读

75 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 22 天,点击查看活动详情

sync.map

sync.Map 是 Go 语言中的一个并发安全的 map,它可以用于多个 goroutine 之间的数据共享和访问。相对于标准库中的 map 类型,sync.Map 的一个显著特点是它是并发安全的,多个 goroutine 可以同时读取和修改其中的数据,而不需要额外的锁机制。此外,与标准库的 map 不同,sync.Map 中的键和值都是接口类型,这使得它更加灵活,可以存储任何类型的数据。同时,由于 sync.Map 内部使用了类似于分片锁的技术,因此在并发访问时也能保持较高的性能。

在使用 sync.Map 时,可以使用其内置的 LoadStoreDeleteRange 方法来读取、写入、删除和遍历其中的数据。与标准库中的 map 类型不同,这些方法的参数和返回值都是接口类型,因此需要进行类型转换才能访问具体的数据。此外,sync.Map 中的数据不支持使用 len 方法来获取长度,因此如果需要统计其中元素的个数,需要使用其他方式来实现。

基本使用

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	m.Store("name", "Alice")
	m.Store("age", 30)

	name, ok := m.Load("name")
	if ok {
		fmt.Println(name)
	}

	age, ok := m.Load("age")
	if ok {
		fmt.Println(age)
	}

	m.Range(func(key, value interface{}) bool {
		fmt.Printf("%s: %v\n", key, value)
		return true
	})
}

源码阅读

type Map struct {
   mu Mutex

   // read 包含了 map 内容的部分,该部分可被并发访问,无论是否持有 mu。
   // read 字段本身是可以安全加载的,但只有持有 mu 时才能进行存储。
   // 存储在 read 中的条目可以在没有 mu 的情况下同时进行更新,但更新先前被删除的条目需要将其复制到 dirty map 中,并在持有 mu 时将其恢复。
   read atomic.Pointer[readOnly]

   // dirty 包含需要持有 mu 才能访问的 map 内容的部分。为了确保 dirty map 可以快速地提升为 read map,它还包括 read map 中的所有未被删除的条目。
   // 被删除的条目不会存储在 dirty map 中。在 clean map 中删除的条目必须在添加新值之前被恢复并添加到 dirty map 中。
   // 如果 dirty map 为 nil,则下一次对 map 的写操作将通过对 clean map 进行浅拷贝(省略陈旧的条目)来初始化它。
   dirty map[any]*entry

   // misses 记录了自从上次更新 read map 以来需要锁定 mu 才能确定键是否存在的加载次数。
   // 一旦出现足够多的 misses 来覆盖复制 dirty map 的成本,dirty map 将被提升为 read map(处于未修改状态),
   // 并且下一次对 map 的 store 将会生成一个新的 dirty map 副本。
   misses int
}

// readOnly 是一个不可变的结构体,原子性地存储在 Map.read 字段中。
type readOnly struct {
   m       map[any]*entry
   amended bool // 如果dirty map包含m中没有的键,则为true。
}

// expunged 是一个任意指针,用于标记已从 dirty map 中删除的条目。
var expunged = new(any)

// 一个 entry 表示 map 中的一个特定键对应的槽位。
type entry struct {
   // p 指向该条目存储的 interface{} 值。
   // 如果 p == nil,则该条目已被删除,且 m.dirty == nil 或 m.dirty[key] == e。
   // 如果 p == expunged,则该条目已被删除,m.dirty != nil,且该条目已从 m.dirty 中删除。
   // 否则,该条目有效,并记录在 m.read.m[key] 中,在 m.dirty != nil 时也会记录在 m.dirty[key] 中。
   // 条目可以通过原子替换为 nil 来删除:当下次创建 m.dirty 时,它将原子替换 nil 为 expunged,并将 m.dirty[key] 保持未设置状态。
   // 如果 p != expunged,则可以通过原子替换来更新条目的关联值。如果 p == expunged,则只能在首先设置 m.dirty[key] = e 以便使用 dirty map 查找条目后才能更新条目的关联值。
   p atomic.Pointer[any]
}
  • mu: Mutex用于保护map的读写操作,确保线程安全。
  • read: atomic.Value用于存储只读状态下的map。在一个线程需要读取map的时候,会先检查这个值,如果不为空则表示这个线程可以直接读取这个值,不需要获取锁。如果为空,则需要获取锁,然后从dirty中获取值。
  • dirty: 存储脏数据的map,即在写操作中被修改但是还没有被写回read的数据。这个map并没有被保护,因为在写操作中,会先获取锁,确保写操作的线程安全。
  • misses: 记录在dirty中未找到的键值对的数量,即缺失的次数,用于调整dirty的大小。

Load

func (m *Map) loadReadOnly() readOnly {
   if p := m.read.Load(); p != nil {
      return *p
   }
   return readOnly{}
}

// Load 方法返回给定 key 在 Map 中对应的 value,如果不存在则返回 nil。
// ok 参数表示是否在 Map 中找到了对应的 value。
func (m *Map) Load(key any) (value any, ok bool) {
   // 原子读取
   read := m.loadReadOnly()
   e, ok := read.m[key]
   // read不存在,dirty可能存在,去dirty看看
   if !ok && read.amended {
      m.mu.Lock()
      // 如果我们在 m.mu 上被阻塞时 m.dirty 得到了晋升,避免报告虚假的缺失。
      // (如果进一步的对同一个键的加载不会丢失,那么为这个键复制脏映射就不值得。)
      read = m.loadReadOnly()
      e, ok = read.m[key]

      // 二次确认
      if !ok && read.amended {
         e, ok = m.dirty[key]
         // 无论条目是否存在,都记录一次未命中:直到将 dirty map 提升为 read map,这个 key 都将采用慢路径。
         m.missLocked()
      }
      m.mu.Unlock()
   }
   // dirty也没有
   if !ok {
      return nil, false
   }
   // read或dirty有,返回结果
   return e.load()
}

// miss+1,并查看是否需要刷新read
func (m *Map) missLocked() {
   m.misses++
   // miss 次数小于dirty长度先不刷新
   if m.misses < len(m.dirty) {
      return
   }
   // 将dirty刷新到read
   m.read.Store(&readOnly{m: m.dirty})
   m.dirty = nil
   m.misses = 0
}

// 返回结果
func (e *entry) load() (value any, ok bool) {
    p := e.p.Load()
    if p == nil || p == expunged {
        return nil, false
    }
    return *p, true
}

Load:

  1. 先看看read中是否有
  2. 如果没有加锁去dirty查看,并且double check
  3. 无论有没有,miss+1,当miss>=dirty.len时将dirty刷新到read
  4. 返回结果

Store

// Store 方法用于为一个键设置值。
func (m *Map) Store(key, value any) {
   _, _ = m.Swap(key, value)
}

// Swap 交换一个键的值并返回原来的值(如果有的话)。
// loaded 的结果报告键是否存在。
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
   read := m.loadReadOnly()
   // read中有
   if e, ok := read.m[key]; ok {
      if v, ok := e.trySwap(&value); ok {
         if v == nil {
            return nil, false
         }
         return *v, true
      }
   }
   // read中没有或操作失败
   m.mu.Lock()
   read = m.loadReadOnly()
   // read中有
   if e, ok := read.m[key]; ok {
      // 之前被删除过
      if e.unexpungeLocked() {
         // 该条目先前被删除,这意味着有一个非空的 dirty map,而这个条目不在其中。
         m.dirty[key] = e
      }
      if v := e.swapLocked(&value); v != nil {
         loaded = true
         previous = *v
      }
      // dirty中有
   } else if e, ok := m.dirty[key]; ok {
      if v := e.swapLocked(&value); v != nil {
         loaded = true
         previous = *v
      }
   } else {
      if !read.amended {
         // 我们将第一个新的键添加到了 dirty map 中。
         // 确保它已被分配,并将只读 map 标记为不完整。
         m.dirtyLocked()
         m.read.Store(&readOnly{m: read.m, amended: true})
      }
      m.dirty[key] = newEntry(value)
   }
   m.mu.Unlock()
   return previous, loaded
}

Delete

// LoadAndDelete 方法会删除指定 key 对应的 value 并返回其值(如果存在)。
// loaded 参数会返回 key 是否存在于 Map 中。
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
   read := m.loadReadOnly()
   e, ok := read.m[key]
   if !ok && read.amended {
      m.mu.Lock()
      // double check
      read = m.loadReadOnly()
      e, ok = read.m[key]
      if !ok && read.amended {
         e, ok = m.dirty[key]
         // 在dirty中删除
         delete(m.dirty, key)
         // 无论entry是否存在,这里都会记录一次miss,因为这个key在dirty map中被删除了,需要在读写锁被持有时,
         // 将其从dirty map复制到read map中。因此,下一次访问这个key时,需要经过一次较慢的查找操作。
         m.missLocked()
      }
      m.mu.Unlock()
   }
   // 在read中删除
   if ok {
      return e.delete()
   }
   return nil, false
}

// Delete 删除指定 key 的值。
func (m *Map) Delete(key any) {
   m.LoadAndDelete(key)
}

总结

image.png

空间换时间,引入额外的dirty,减少了read的压力,read不需要加锁。 以下两个场景中使用 sync.Map,会比使用 map+RWMutex 的方式,性能要好得多:

  • 只会增长的缓存系统中,一个 key 只写入一次而被读很多次;
  • 多个 goroutine 为不相交的键集读、写和重写键值对。