一、前言

在很多情景中,我们需要使用到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不会发生“抖动”。

转自:https://juejin.cn/post/7114865009766694926