深入理解golang:sync.map

本文详细探讨了Go语言中内置的并发不安全的map以及为何需要sync.map。通过示例展示了并发写map导致的错误,解释了sync.map的实现原理,包括读写分离、延迟更新等特性,以及在读多写少场景下的性能优势。此外,还提到了concurrent-map第三方库作为另一种并发控制的选择。
摘要由CSDN通过智能技术生成

疑惑开篇#
有了map为什么还要搞个sync.map 呢?它们之间有什么区别?
答:重要的一点是,map并发不是安全的。

在Go 1.6之前, 内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。自go 1.6之后, 并发地读写map会报错,这在一些知名的开源库中都存在这个问题,所以go 1.9之前的解决方案是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。

go version go1.13.9 windows/amd64

测试一波#
写一个简单的并发写map的测试代码看看:
testcurmap.go

Copy
package main

import (
“fmt”
“time”
)

func main() {
m := map[string]int{“age”: 10}

go func() {
	i := 0
	for i < 1000 {
		m["age"] = 10
		i++
	}
}()

go func() { //19 行
	i := 0
	for i < 1000 {
		m["age"] = 11 //22 行
		i++
	}
}()

time.Sleep(time.Second * 3)
fmt.Println(m)

}
多运行几次:go run testcurmap.go
会报错,错误的扼要信息如下:

fatal error: concurrent map writes

goroutine 7 [running]:
runtime.throw(0x4d49a3, 0x15)
/go/src/runtime/panic.go:774 +0x79 fp=0xc000041f30 sp=0xc000041f00 pc=0x42cf19
runtime.mapassign_faststr(0x4b4360, 0xc000066330, 0x4d168a, 0x3, 0x0)
/go/src/runtime/map_faststr.go:211 +0x41e fp=0xc000041f98 sp=0xc000041f30 pc=0x410f8e
main.main.func2(0xc000066330)
/mygo/src/study/go-practice2/map/curmap/testcurmap.go:22 +0x5c fp=0xc000041fd8 sp=0xc000041f98 pc=0x49ac9c
runtime.goexit()
/go/src/runtime/asm_amd64.s:1357 +0x1 fp=0xc000041fe0 sp=0xc000041fd8 pc=0x455391
created by main.main
/mygo/src/study/go-practice2/map/curmap/testcurmap.go:19 +0xb0

exit status 2

看报错信息是src/runtime/map_faststr.go:211 这个函数runtime.mapassign_faststr,它在runtime/map_faststr.go 中,简要代码如下:

Copy
func mapassign_faststr(t *maptype, h *hmap, s string) unsafe.Pointer {
… …

if h.flags&hashWriting != 0 {
	throw("concurrent map writes")
}

... ...

}
hashWriting = 4 // a goroutine is writing to the map goroutine写的一个标识,
这里h.flags与自己进行与运算,判断是否有其他goroutine在操作这个map,不是0说明有其他goroutine操作map,所以报错。

那咋防止map并发呢,一般有几种方式:

map+Mutex:
给map加一把大锁
map+RWMutex
给map加一个读写锁,给锁细分。适合读多写少场景
修改一下程序#
加一把读写锁防止并发,修改程序 testcurmap2.go:

Copy
package main

import (
“fmt”
“sync”
“time”
)

func main() {
m := map[string]int{“age”: 10}

var s sync.RWMutex
go func() {
	i := 0
	for i < 1000 {
		s.Lock()
		m["age"] = 10
		s.Unlock()
		i++
	}
}()

go func() {
	i := 0
	for i < 1000 {
		s.Lock()
		m["age"] = 11
		s.Unlock()
		i++
	}
}()

time.Sleep(time.Second * 3)
fmt.Println(m)

}
运行结果:
map[age:11]

没有报错了。

就到这里了吗?可以在思考思考,还有其他方法控制并发的方法没?有的,sync.map 登场

控制并的第三种方式:

sync.Map
官方实现的并发map。
原理是通过分离读写map和原子指令来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化。一般情况下可以替换上面2种锁。
sync.map#
先看一个简单的代码 testcurmap3.go

Copy
package main

import (
“fmt”
“sync”
“time”
)

func main() {
smap := sync.Map{}

smap.Store("age", 10)

go func() {
	i := 0
	for i < 1000 {
		smap.Store("one", 10)
		i++
	}
}()

go func() {
	i := 0
	for i < 1000 {
		smap.Store("one", 11)
		i++
	}
}()

time.Sleep(time.Second * 2)
fmt.Println(smap.Load("one"))

}
运行输出:11 true
正常输出,没有报错。

sync.Map 的主要思想就是读写分离,空间换时间。

看看 sync.map 优点:

空间换时间:通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
使用只读数据(read),避免读写冲突。
动态调整,miss次数多了之后,将dirty数据迁移到read中。
double-checking。
延迟删除。 删除一个键值只是打标记,只有在迁移dirty数据的时候才清理删除的数据。
优先从read读取、更新、删除,因为对read的读取不需要锁。
sync.Map 数据结构#
Map 数据结构#
在 src/sync/map.go 中

Copy
type Map struct {
// 当涉及到脏数据(dirty)操作时候,需要使用这个锁
mu Mutex

// read是一个只读数据结构,包含一个map结构,
// 读不需要加锁,只需要通过 atomic 加载最新的指正即可
read atomic.Value // readOnly

// dirty 包含部分map的键值对,如果操作需要mutex获取锁
// 最后dirty中的元素会被全部提升到read里的map去
dirty map[interface{}]*entry

// misses是一个计数器,用于记录read中没有的数据而在dirty中有的数据的数量。
// 也就是说如果read不包含这个数据,会从dirty中读取,并misses+1
// 当mi
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值