Golang 基础之并发基本同步原语(三)

大家好,今天将梳理出的 Go语言并发知识内容,分享给大家。 请多多指教,谢谢。

本文主要介绍 sync 标准库中的 sync.Map 介绍及使用。

本章节内容

  • sync.Map

sync.Map

介绍

sync.Map 类似于 Go内置 Map[interface{}]interface{},但对于多个 goroutine 并发使用是安全的,无需额外的锁或协调。加载、存储和删除在平摊常数时间内运行。

sync.Map类型针对两个常见的用例进行了优化:

  1. 当一个给定键的条目只写入一次而被多次读取时,就像在缓存中只会增长一样。

  2. 当多个 goroutine 对不相交的键集读、写和覆盖条目时。在这两种情况下,与单独 MutexRWMutex 配对的Go Map相比,使用 sync.Map 类型可以显著减少锁争用。

总之来说适合大量读,少量写。

历史版本

在Go v1.6之前,内置 map 是部分 goroutine 安全的,并发读没有问题,并发写可能有问题。

在Go v1.6之后,并发读写内置 map 会报错,在一些知名的开源库都有这个问题,所以在Go v1.9之前,解决方案是加一个额外的大锁,锁住map。

在Go v1.9之后,Go官方推出了 Mapsync.Map

sync.Map 类型原型

// entry 键值对中的值结构体
type entry struct {
	p unsafe.Pointer // 指针,指向实际存储value值的地方
}

// Map 并发安全的map结构体
type Map struct {
	mu sync.Mutex // 锁,保护read和dirty字段

	read atomic.Value // 存仅读数据,原子操作,并发读安全,实际存储readOnly类型的数据

	dirty map[interface{}]*entry // 存最新写入的数据

	misses int // 计数器,每次在read字段中没找所需数据时,+1
	// 当此值到达一定阈值时,将dirty字段赋值给read
}

// readOnly 存储mao中仅读数据的结构体
type readOnly struct {
	m       map[interface{}]*entry // 其底层依然是个最简单的map
	amended bool                   // 标志位,标识m.dirty中存储的数据是否和m.read中的不一样,flase 相同,true不相同
}

func (m *Map) Delete(key any)
func (m *Map) Load(key any) (value any, ok bool)
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
func (m *Map) Range(f func(key, value any) bool)
func (m *Map) Store(key, value any)

sync.Map 中key和value是分开存放的,key通过内置map指向entry,entry通过指针,指向value实际内存地址。

需要注意的地方:

  • read在进行非读操作时,需要锁mu进行保护
  • 写入的数据,都是直接写到dirty,后面根据read miss次数达到阈值,会进行read和dirty数据的同步
  • readOnly中专门有一个标志位,用来标注read和dirty中是否有不同,以便进行read和dirty数据同步

func (m *Map) Delete(key any) 方法:删除键的值。

func (m *Map) Load(key any) 方法: 返回存储在映射中的某个键的值,如果没有值,则返回nil。ok结果表明是否在映射中找到了值。

func (m *Map) LoadAndDelete(key any) 方法:删除键的值,如果有则返回前一个值。加载的结果报告键是否存在。

func (m *Map) LoadOrStore(key, value any) 方法:如果存在,则返回键的现有值。否则,它存储并返回给定的值。如果该值已加载,则加载结果为true,如果已存储则为false。

func (m *Map) Range(f func(key, value any) bool) 方法:Range对映射中出现的每个键和值依次调用f。如果f返回false, range将停止迭代。

func (m *Map) Store(key, value any) 方法:设置键的值。

sync.Map 设计思想

空间换时间

sync.Map 中冗余的数据结构就是 dirtyread,二者存放的都是 key-entryentry 其实是一个指针,指向 valuereaddirty 各自维护一套 keykey 指向的都是同一个 value ,也就是说,只要修改了这个entry,对 readdirty 都是可见的。

拿空间换时间策略在 sync.Map 中的体现:

  • 遍历操作:只需遍历read即可,而read是并发读安全的,没有锁,相比于加锁方案,性能大为提升
  • 查找操作:先在read中查找,read中找不到再去dirty中找

核心思想就是一切操作先去read中执行,因为read是并发读安全的,无需锁,实在在read中找不到,再去dirty中。read在sycn.Map 中是一种冗余的数据结构,因为read和dirty中数据有很大一部分是重复的,而且二者还会进行数据同步。

读写分离

sync.Map 中有专门用于读的数据结构:read,将其和写操作分离开来,可以避免读写冲突。而采用读写分离策略的代价就是冗余的数据结构,其实还是空间换时间的思想。

双检查机制

sync.Map 中,每次当 read 不符合要求要去操作 dirty 前, 都会上锁, 上锁后再次判断是否符合要求, 因为 read 有可能在上锁期间,产生了变化,突然又符合要求了。

通过额外的一次检查操作,来避免在第一次检查操作完成后,其他的操作使得检查条件产生突然符合要求的可能。

延迟删除

在删除操作中,删除 key-value 仅仅只是先将需要删除的 key-value 打一个标记,这样可以尽快的让 delete 操作先返回,减少耗时,在后面提升 dirty 时,在一次性的删除需要删除的 key-value。

read 优先

需要进行读取、删除、更新操作时,优先操作 read,因为 read 无锁的,更快。 如果在 read 中得不到结果,再去 dirty中。

read 的修改操作需要加锁, read只是并发读安全,并发写并不安全。

状态机制

entry 的指针是有状态的,主要分为:nil、expunged(指向被删除的元素)、正常状态。

主要是两个操作会引起 entry指针状态的变化:Store()(新增/修改)和 Delete()(删除)

sync.Map 实践

基本使用
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map
	// 写入
	m.Store("key1", 1)
	m.Store("key2", 2)
	// 读取
	value, _ := m.Load("key1")
	fmt.Println(value.(int))
	// 遍历
	m.Range(func(key, value interface{}) bool {
		k := key.(string)
		v := value.(int)
		fmt.Println(k, v)
		return true
	})
	// 删除
	m.Delete("key1")
	value, ok := m.Load("key1")
	fmt.Println(value, ok)
	// 读取或写入
	m.LoadOrStore("key2", 22)
	value, _ = m.Load("key2")
	fmt.Println(value)
}

输出

1
key1 1
key2 2
<nil> false
2

第 1 步,写入两个 k-v 对;

第 2 步,使用 Load 方法读取其中的一个 key;

第 3 步,遍历所有的 k-v 对,并打印出来;

第 4 步,删除其中的一个 key,再读这个 key,得到的就是 nil;

第 5 步,使用 LoadOrStore,尝试读取或写入 “key2”,因为这个 key 已经存在,因此写入不成功,并且读出原值。

解决并发中write问题

golang的map是非协程安全的,并发写是会出现错误

package main

func main() {
	m := map[int]int{1:1}
	go concurrent(m)
	go concurrent(m)
	select{}
}

func concurrent(m map[int]int) {
	i := 0
	for i < 10000 {
		m[1] = 1 // 频繁写
		i++
	}
}

输出

fatal error: concurrent map writes

在多个goroutine中,map不能同时写。

使用 sync.Map 解决不能同时写问题

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	m := sync.Map{}
	m.Store(1, 1)
	go concurrent(m)
	go concurrent(m)
	time.Sleep(3*time.Second)
	fmt.Println(m.Load(1))
}

func concurrent(m sync.Map) {
	i := 0
	for i < 10000 {
		m.Store(1, 1) // 频繁写
		i++
	}
}

输出

1 true
互斥锁与sync.Map效率对比
package main

import (
	"fmt"
	"time"
	"sync"
)

var s sync.RWMutex
var w sync.WaitGroup

func main() {
	mapTest()
	syncMapTest()
}

func mapTest() {
	m := map[int]int {1:1}
	startTime := time.Now().Nanosecond()
	w.Add(1)
	go writeMap(m)
	w.Add(1)
	go writeMap(m)
	w.Add(1)
	go readMap(m)

	w.Wait()
	endTime := time.Now().Nanosecond()
	timeDiff := endTime-startTime
	fmt.Println("map:",timeDiff)
}

func writeMap (m map[int]int) {
	defer w.Done()
	i := 0
	for i < 10000 {
		// 加锁
		s.Lock()
		m[1]=1
		// 解锁
		s.Unlock()
		i++
	}
}

func readMap (m map[int]int) {
	defer w.Done()
	i := 0
	for i < 10000 {
		s.RLock()
		_ = m[1]
		s.RUnlock()
		i++
	}
}

func syncMapTest() {
	m := sync.Map{}
	m.Store(1,1)
	startTime := time.Now().Nanosecond()
	w.Add(1)
	go writeSyncMap(m)
	w.Add(1)
	go writeSyncMap(m)
	w.Add(1)
	go readSyncMap(m)

	w.Wait()
	endTime := time.Now().Nanosecond()
	timeDiff := endTime-startTime
	fmt.Println("sync.Map:",timeDiff)
}

func writeSyncMap (m sync.Map) {
	defer w.Done()
	i := 0
	for i < 10000 {
		m.Store(1,1)
		i++
	}
}

func readSyncMap (m sync.Map) {
	defer w.Done()
	i := 0
	for i < 10000 {
		m.Load(1)
		i++
	}
}

对比结果如下:

情况结果
只写map: 1374000 sync.Map: 2899000
读写map: 7607000 sync.Map: 22295000

总结:在大量写的场景下, sync.Map 的效率没有单纯 map + Mutex的效率高。 读写场景下因为互斥锁大量消耗解锁加锁,性能消耗 sync.Map 最优。

技术文章持续更新,请大家多多关注呀~~

搜索微信公众号,关注我【 帽儿山的枪手 】

参考材料

  • sync.Map 设计思想和底层源码分析
  • 由浅入深聊 Golang的 sync.Map
  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值