面经-并发安全的切片/map

105 阅读3分钟
package main

import (
   "fmt"
   "sync"
)

//高并发写入

//1. 加锁


func main1() {
   slc := []int{}

   n := 10000
   var wg sync.WaitGroup
   var lock sync.Mutex
   wg.Add(n)
   for i := 0; i < n; i++ {
      go func(a int) {
         lock.Lock()
         slc = append(slc, a)
         lock.Unlock()
         wg.Done()
      }(i)
   }
   wg.Wait()

   fmt.Println("done len:", len(slc))
}

//sync.Map

func main() {
   var scene sync.Map
   //将键值对保存到sync.Map
   scene.Store("greece", 97)
   scene.Store("london", 100)
   scene.Store("egypt", 200)
   // 从sync.Map中根据键取值
   fmt.Println(scene.Load("london"))
   // 根据键删除对应的键值对
   scene.Delete("london")
   // 遍历所有sync.Map中的键值对
   scene.Range(func(k, v interface{}) bool {
      fmt.Println("iterate:", k, v)
      return true
   })


}

在golang中map不是并发安全的,所有才有了sync.Map的实现,尽管sync.Map的引入确实从性能上面解决了map的并发安全问题,不过sync.Map却没有实现len()函数,这导致了在使用sync.Map的时候,一旦需要计算长度,就比较麻烦,

func main1() {
   slc := map[int]int{}

   var wg sync.WaitGroup
   var lock sync.Mutex
   i := 0
   
   for i <1000 {
       wg.Add(1)
      go func(a int) {
      
          defer wg.Done()
         lock.Lock()
         slc[1] = i
         lock.Unlock()
         
      }()
      i++
   }
   wg.Wait()

   fmt.Println(slc)
}

虽然这样map可以在工程里面使用,但是效果不好,毕竟加锁的话,就是对整个map进行加锁和解锁,导致整个map在加锁的过程中都被阻塞住,这种操作会严重影响性能。

func main02() {
   test := sync.Map{}
   var wg sync.WaitGroup
   i := 0
   for i < 1000 {
      wg.Add(1)
      go func() {
         defer wg.Done()
         test.Store(1,i)
      }()
      i++
   }

   wg.Wait()
   fmt.Println(test.Load(1))
}

计算长度

func main() {
   test := sync.Map{}
   var wg sync.WaitGroup
   i := 0
   for i < 1000 {
      wg.Add(1)
      go func(i int) {
         defer wg.Done()
         test.LoadOrStore(i, 1)
      }(i)
      i++
   }
   wg.Wait()
   len := 0
   test.Range(func(k, v interface{}) bool {
      len++
      return true
   })
   fmt.Println("len of test:",len)
}

map加锁和sunc.map的区别:

在高并发场景下,使用互斥锁对map进行加锁会存在以下两个问题:

  1. 高并发下锁的竞争激烈。当多个goroutine同时访问同一个map时,它们之间会产生大量的锁竞争,而这种锁竞争会导致大量的线程切换和CPU时间的浪费,同时也会影响程序的性能表现。
  2. 锁粒度过大。由于map是一种非常常用的数据结构,可能会被众多的goroutine共享访问。如果直接用互斥锁来保护整个map,那么在高并发情况下,每次操作都需要等待获取锁,即使只有少量数据需要修改,也需要等待其他goroutine释放锁后才能进行。这样就会导致锁的粒度过大,降低了并发性能。

sync.Map则是针对这种情况而设计的。它内部采用了一些优化技术来避免锁的过多使用和降低锁的粒度。具体来说:

  1. sync.Map内部的键值对是通过多个bucket而非单一的哈希表实现的,不同的bucket之间没有锁竞争。这样就可以将锁的粒度缩小到每个bucket级别。
  2. sync.Map内部的锁被设计成了分段锁,不同的goroutine可以同时访问不同的bucket,降低了锁竞争的概率。在高并发环境下更加高效,可以提供更好的性能表现。
  3. sync.Map还提供了一些方法来支持安全的并发操作,例如Load()Store()Delete()等方法都是原子的,多个goroutine之间调用它们是没有竞争关系的,因此在高并发场景下使用更加安全可靠。

虽然使用互斥锁对map进行加锁也可以实现并发安全,但在高并发场景下锁的竞争会非常激烈,性能可能会有明显下降。而sync.Map则是针对这种情况而设计的,不仅提供了更好的并发安全保障,同时也优化了在高并发情况下的性能表现。

map遍历是无序的嘛?

不是的,Go语言中的map遍历是无序的。这是因为map的实现方式决定了其元素在内存中的存储顺序不固定,而且由于哈希函数的作用,不同的key可能会被分配到不同的位置上去。因此,在使用map遍历时不能依赖遍历的顺序,如果需要按照key或value的排序进行遍历,需要通过其他的数据结构来辅助实现。