一、前言
在很多情景中,我们需要使用到map类型,但是对于go语言来说,出于高效简单的设计理念,原生的map类型并不是线程安全的。我们可以运行一段代码进行验证:
package main
func main() {
mp := make(map[string]string)
go func() {
for {
mp["hello"] = "world"
}
}()
go func() {
for {
_ = mp["hello"]
}
}()
select {}
}
在上述代码中,启动了两个协程分别不断地进行读操作和写操作,并使用select进行主进程的阻塞。
运行后,产生了恐慌,抛出异常信息fatal error: concurrent map read and map write:
fatal error: concurrent map read and map write
goroutine 18 [running]:
runtime.throw(0x4793c1, 0x21)
/usr/local/go/src/runtime/panic.go:1117 +0x72 fp=0xc000038f30 sp=0xc000038f00 pc=0x42e972
runtime.mapaccess1_faststr(0x4696e0, 0xc000096000, 0x475f73, 0x5, 0xc000064088)
/usr/local/go/src/runtime/map_faststr.go:21 +0x465 fp=0xc000038fa0 sp=0xc000038f30 pc=0x40eca5
main.main.func2(0xc000096000)
二、读写锁
最原始的方法,是在原有map的基础上继承读写锁,对读写操作进行加锁控制并编写成方法:
type MyMap struct {
sync.RWMutex
mp map[string]string
}
func (m *MyMap) Get(key string) (value string, ok bool) {
m.RLock()
defer m.RUnlock()
value, ok = m.mp[key]
return
}
func (m *MyMap) Set(key, value string) {
m.Lock()
defer m.Unlock()
m.mp[key] = value
}
完成修改后,对原有的测试代码进行修改,将map类型换为刚刚定义的新map类型,然后使用自定义的Get和Set方法对读写操作进行修改:
func main() {
mp := MyMap{
RWMutex: sync.RWMutex{},
mp: make(map[string]string),
}
go func() {
for {
mp.Set("hello", "world")
}
}()
go func() {
for {
t, _ := mp.Get("hello")
fmt.Println(t)
}
}()
select {}
}
运行后,正常输出结果值,并未再发生冲突:
world
world
world
world
world
world
world
world
world
world
world
world
world
三、sync.Map
go在后续的版本更新(1.9)中,实现了线程安全的sync.Map类型,其原理是将读写进行分离,然后优先执行读操作,避免了每步操作都需要进行锁操作的情况。
对于读写操作分别使用Load和Store方法实现,再次重写测试用例如下:
func main() {
mp := sync.Map{}
go func() {
for {
mp.Store("hello", "world")
}
}()
go func() {
for {
t, _ := mp.Load("hello")
fmt.Println(t)
}
}()
select {}
}
运行后同样不会产生恐慌:
world
world
world
world
world
world
world
world
world
world
四、性能对比
以sync.Map为例,编写一个测试运行时间的函数,并可以由我们指定读写次数,通过channel+select来阻塞进程并通过time进行计时:
func syncMapTest(readNum, writeNum int) {
mp := sync.Map{}
start := time.Now()
readEnd, writeEnd := make(chan bool), make(chan bool)
go func() {
for i := 0; i < writeNum; i++ {
mp.Store("hello", "world")
}
writeEnd<-true
}()
go func() {
for i := 0; i < readNum; i++ {
_, _ = mp.Load("hello")
}
readEnd<-true
}()
for i := 0; i < 2; i++ {
select {
case <-readEnd:
fmt.Println("syncMap读已完成,时间花费:", time.Since(start))
case <-writeEnd:
fmt.Println("syncMap写已完成,时间花费:", time.Since(start))
}
}
}
在读写次数相同的情况下,sync.Map的效率要比读写锁实现的map效率高上一个量级
五、总结
根据测试结果,对于高写入低读取的场合并需要保证读取效率的场合可以使用sync.Map效果较佳。
虽然由于MyMap频繁进行锁操作效率较低,但是稳定较强,相比sync.Map不会发生“抖动”。