go sync.Map Range 的同时进行 Store,Range 的遍历结果如何?(源码分析)

背景

最近翻看之前的代码,发现使用了 sync.Map,并对其异步做了 StoreRange 的操作。

RangeStore 异步,能够遍历到后添加的数据吗?带着这个问题,翻了下源码,简单了解其原理。先说结论:可能会遍历到 Store 添加的数据的。

查看本篇文章时,最好对 sync.Map 的源码有个初步的了解。

测试代码

先来看一段代码,输出什么?

package main

import (
	"fmt"
	"sync"
)

func main() {
    
    
	var wg sync.WaitGroup
	type cans struct {
    
    
		t int
		v any
	}
	var c = make(chan cans, 10000)

	m := new(sync.Map)
	m.Store("a", "a")
	m.Store("b", "b")
	m.Store("c", "c")

	var ccc = make(chan struct{
    
    })
	wg.Add(1)
	go func() {
    
    
		defer wg.Done()
		for i := 0; i < 1000; i++ {
    
    
			if i == 1 {
    
    
				ccc <- struct{
    
    }{
    
    }
			}
			m.Store(i, i)
			c <- cans{
    
    1, i}
		}
		fmt.Println("store end")
	}()
	wg.Add(1)
	go func() {
    
    
		<-ccc
		defer wg.Done()
		// range 的时候会判断是否有 dirty 数据,有的话也会去竞争锁,当拿到锁的那一刻,会清空 dirty 同步 read 然后会进入 for 去回调
		m.Range(func(key, value any) bool {
    
    
			c <- cans{
    
    2, key}
			return true
		})
		fmt.Println("range end")
	}()

	wg.Wait()
	close(c)

	fmt.Println("channel 长度:", len(c))

	for v := range c {
    
    
		fmt.Println(v.t, ": ", v.v)
	}

	fmt.Println("end")
}

测试结果:Range 输出的结果是包含了一部分另一个协程中 Store 进入的数据的。

源码解读

  • 通过 readdirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
  • 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
  • 读取 read 并不需要加锁,而读或写 dirty 都需要加锁
  • 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read
  • 对于删除数据则直接通过标记来延迟删除
  • Range 遍历时会先判断 dirty 中是否有 read 中不存在的数据,有则加锁,同步至 read 后解锁 ,遍历当前的 read

sync.Map 的数据结构

type Map struct {
    
    
    // 加锁作用,保护 dirty 字段
    mu Mutex
    // 只读的数据,实际数据类型为 readOnly
    read atomic.Value
    // 最新写入的数据
    dirty map[interface{
    
    }]*entry
    // 计数器,每次需要读 dirty 则 +1
    misses int
}

readOnly 数据结构

type readOnly struct {
    
    
    // 内建 map
    m  map[interface{
    
    }]*entry
    // 表示 dirty 里存在 read 里没有的 key,通过该字段决定是否加锁读 dirty
    amended bool
}

entry 数据结构则用于存储值的指针

type entry struct {
    
    
    p unsafe.Pointer  // 指针
}

总结

再详细解释一下开头提出的问题,Range Store 并发的时候,Range 的遍历结果如何。

  1. Range 先开始,并且没有发现 dirty 中有 read 中不存在的数据(即:m.read.amendedfalse),则遍历过程中是不能够获取到新添加进 Map 中的数据的,因为遍历的是之前的副本。
  2. Store 先开始,Range 遍历的时候会发现 dirty 中有 read 中不存在的数据,则会去和 Store 去竞争同一把锁,在 Range 竞争到锁的时候,会清空 dirty 同步 read 然后会拿着当前 read 的副本进入 for 去回调,即:遍历过程中可能会读取到 Store 的一部分的数据。

Range 的时候能不能获取到新 Store 的数据,主要是看谁先获取到锁。若是 Store 先获取到,则 Range 的时候还是能遍历到的,若是 Range 先获取到锁,则只会遍历当前 Map 中已有的数据。

sync.Map 的读写分离设计,解决了并发情况的写入安全,又使读取速度在大部分情况可以接近内建 map,非常适合读多写少的情况。

参考

源码解读 Golang 的 sync.Map 实现原理

猜你喜欢

转载自blog.csdn.net/DisMisPres/article/details/127104332