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 实现原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值