Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go语言Map

Go语言针对Map的11问你知道几个?

作者:程序员祝融

Go Map 的 11 连问,你顶得了嘛?这篇文章小编为大家准备了 Go 语言 Map 的 11 连问,相信大家看完肯定会有帮助的,感兴趣的小伙伴可以收藏一波

1. Map 使用时需要注意哪些问题?

2. Map 扩容是怎么实现的?

在 Go 中,Map 的内部实现是基于哈希表(hash table)的,因此,当 Map 中的键值对数量增加时,Map 会自动扩容以提供更多的存储空间。下面是 Go Map 扩容的具体步骤:

注意:  由于扩容操作可能会导致大量的重新散列操作,因此在扩容时可能会造成一定的性能影响。为了避免频繁扩容,可以在创建 Map 时指定一个较大的容量,或者手动调用 runtime.GC()  来触发垃圾回收操作,以释放未使用的内存。

3. Map 的 panic 能被 recover 吗?

不能,并发读写 Map 也是很容易遇到的问题。如下代码可以验证:

package main
import (
  "fmt"
)
func foo(){
  defer func(){
      if err := recover(); err != nil {
          fmt.Println(err)
      }
  }()
  var bar = make(map[int]int)
  go func(){
      defer func(){
          if err := recover(); err != nil {
              fmt.Println(err)
          }
      }()
      for{
          _ = bar[1]
      }
  }()
  for{
      bar[1]=1
  }
}
func main(){
  foo()
  fmt.Println("exit")
}

输出:

fatal error: concurrent map read and map write

goroutine 18 [running]:
runtime.throw({0x1021bdf62?, 0x0?})
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/panic.go:992 +0x50 fp=0x14000046f70 sp=0x14000046f40 pc=0x10215f050
runtime.mapaccess1_fast64(0x0?, 0x0?, 0x1)
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/map_fast64.go:22 +0x174 fp=0x14000046f90 sp=0x14000046f70 pc=0x10213eaa4
main.foo.func2()
        /Users/xxx/go/learn/main.go:43 +0x50 fp=0x14000046fd0 sp=0x14000046f90 pc=0x1021b8a00
runtime.goexit()
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/asm_arm64.s:1270 +0x4 fp=0x14000046fd0 sp=0x14000046fd0 pc=0x10218aa64
created by main.foo
        /Users/xxx/go/learn/main.go:36 +0x84

goroutine 1 [runnable]:
main.foo()
        /Users/xxx/go/learn/main.go:47 +0x98
main.main()
        /Users/xxx/go/learn/main.go:52 +0x20
exit status 2

输出日志里没有出现我们在程序末尾打印的 exit,而是直接将调用栈打印出来了。查看底层 /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/map_fast64.go:22 中的代码不难发现这几行:

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

小结一下:

4. 并发使用 Map 除了加锁还有什么其他方案吗?

除了加锁之外,Go 并发使用 Map 的其他常见解决方案包括使用 sync.Map 和使用并发安全的第三方 Map 库。

1、使用 sync.Map sync.Map 是 Go 1.9 新增的一种并发安全的 Map,它的读写操作都是并发安全的,无需加锁。使用 sync.Map 的示例代码如下:

var m sync.Map
m.Store("name", "程序员祝融")
value, ok := m.Load("name")
if ok {
    fmt.Println(value)
}
// 输出程序员祝融

2、使用并发安全的第三方 Map 库 除了使用 sync.Map,还可以使用其他第三方的并发安全的 Map 库,如 concurrent-map、ccmap 等。这些库的使用方式与 Go 标准库的 Map 类似,但它们都提供了更高效、更可靠的并发安全保证。

注意:  使用并发安全的第三方 Map 库可能会导致额外的依赖和复杂性,因此需要仔细评估是否值得使用。

5. sync.Map 和加锁的区别是什么?

因此,当需要在并发访问时安全地共享数据时,可以考虑使用 sync.Map 来避免竞争条件和数据竞争问题,并提高性能。而当需要在使用锁时,需要显式地加锁和解锁来保证数据一致性和避免竞争条件。

6. Map 的查询时间复杂度?

Go 中的 Map 底层实现采用了哈希表,因此其查询时间复杂度为 O(1),最坏情况为 O(n)。

7.Map Rehash 的策略是怎样的?什么时机会发生 Rehash?

当哈希表中的元素数量达到一定阈值时,就会触发哈希表的扩容操作,也就是进行 rehash。

Go 标准库中的哈希表实现在以下情况下会触发 rehash 操作:

在进行 rehash 操作时,哈希表会创建一个新的数组来存储重新哈希后的元素,然后将旧数组中的元素逐个复制到新数组中。由于重新哈希的过程比较耗时,因此 Go 标准库中的哈希表实现采用了增量式 rehash 策略,在扩容和收缩时只会处理一部分元素,避免一次性处理过多元素导致性能下降。具体来说,增量式 rehash 的策略是:

通过增量式 rehash 的策略,Go 标准库中的哈希表实现可以在保证较短的 rehash 时间的同时,避免一次性处理过多元素导致性能下降。

8. Map Rehash 具体会影响什么?哈希结果会受到什么影响?

rehash 操作会影响 Map 的性能。由于重新计算键的哈希值,rehash 操作会消耗一定的计算资源。此外,在 rehash 过程中,原始哈希表的所有键值对都需要复制到新的哈希表中,因此 rehash 操作还会消耗一定的内存空间和时间。

rehash 操作不会直接影响哈希结果。哈希结果是由哈希函数计算得出的,与 Map 中元素的数量和布局无关。rehash 操作只会影响哈希表的布局,即每个键在哈希表中的位置会发生变化,但是每个键的哈希值并不会改变。

9. Map Rehash 过程中存放在旧桶的元素如何迁移?

rehash 操作会创建一个新的哈希表,同时保留旧的哈希表不变。然后,它会依次遍历旧哈希表中的每个桶,将桶中的所有键值对复制到新哈希表中对应的桶中。在遍历每个桶时,如果桶中的元素已经被删除,则跳过这些元素。

当遍历到一个桶时,Map 实现会首先获取旧哈希表中该桶的互斥锁,以确保在复制元素时不会有并发访问的问题。然后,它会遍历桶中的所有键值对,对于每个键值对,它会计算键在新哈希表中的位置,并将键值对插入到对应的桶中。插入过程中,如果新哈希表中对应的桶已经存在元素,则会将新元素插入到该桶的链表的尾部。

在整个 rehash 过程中,新哈希表会保持为空,直到所有元素都被复制完毕。复制完成后,Map 实现会用新哈希表替换旧哈希表,并释放旧哈希表占用的内存空间。这样,Map 中的所有元素都被迁移到了新哈希表中。

需要注意的是,在 rehash 过程中,如果有并发访问 Map,则可能会发生竞争条件。因此,Go 语言中的 Map 不是线程安全的。如果需要在多个 goroutine 中访问 Map,则需要使用互斥锁或其他并发安全的机制来保护访问。

10. sync.Map 的 Load() 方法流程?

sync.Map 中的 Load() 方法用于获取指定键对应的值,其流程如下:

注意:虽然 sync.Map 可以在多个 goroutine 中安全地访问和修改,但由于其内部仍然使用锁来保证并发安全,因此在高并发场景下,其性能可能不如非并发安全的 map 类型。因此,在并发性能要求较高的场景下,建议使用更为轻量级的并发控制机制,例如互斥锁和读写锁,  来保护普通的 map

11. sync.Map Store() 如何保持缓存层和底层 Map 数据是相同的?

在 Go 语言的 sync.Map 中,Store() 方法用于向映射中存储一个键值对。为了保持缓存层和底层 map 数据的一致性,sync.Map 使用了一种特殊的机制,具体流程如下:

注意:  sync.Map 使用缓存层和底层 map 之间的转换机制来避免锁的使用,从而提高了并发性能。然而,由于缓存层和底层 map 之间存在一定的延迟和不一致性,因此在一些特殊的场景下,可能会出现一些数据同步的问题,需要特别注意。

到此这篇关于Go语言针对Map的11问你知道几个?的文章就介绍到这了,更多相关Go语言Map内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文