前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >什么?你现在都还不会处理并发 map 的问题,这就安排...

什么?你现在都还不会处理并发 map 的问题,这就安排...

作者头像
小锟哥哥
发布2022-05-10 08:59:43
1500
发布2022-05-10 08:59:43
举报
文章被收录于专栏:GoLang全栈

key-value 类型的数据结构,在项目中是非常常见的数据结构,属于基础的数据结构。

因为他每一个 key 都是唯一的索引值,所以能够通过索引很快的找到对应的值。

在 Go 语言中对应的数据结构就是 Map。

但是,Go 里面的 Map 数据结构,他并不是线程安全的,一但并发操作就会出现各种问题。

或许你现在项目中还没遇到,但是墨菲定律告诉我们,“凡是可能出错的事,必定会出错”;

所以,你可以将这篇文章加入收藏,踩坑了再来排坑。

使用Map常见的2种错误

错误一:未初始化

Go 语言中 map 和 slice 不同,map 对象必须在使用之前初始化。

如果不初始化就直接赋值的话,就会出现 panic 异常,比如下面的代码:

代码语言:javascript
复制
var m map[int]int
m[1]=1

执行后会报错:

代码语言:javascript
复制
$ go run map.go 
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
        /Users/kun/Desktop/课件/bingfa-demo/map.go:6 +0x27
exit status 2

非常好玩的是,不能赋值,但是却可以取值:

代码语言:javascript
复制
var m map[int]int
fmt.Println(m[1])

执行的结果是:

代码语言:javascript
复制
$ go run map.go 
0

造成这个好玩的现象是因为:

从一个 nil 的 map 对象中获取值,并不会 panic,而是会得到一个零值。

所以切记 在使用 map 这个数据类型时,一定一定要初始化,目前还没有工具可以检查是否初始化了,所以我们只能疯狂的记住它!

错误二:并发读写

这是一个非常让人头大的问题,经常会出现本地调试OK,上线一出现并发就 panic,先来看一段代码:

代码语言:javascript
复制
var m = make(map[int]string,10) // 初始化一个map
go func() {
 for {
  m[1] = "hello" //设置key
 }
}()

go func() {
 for {
  _ = m[2] //访问这个map
 }
}()
select {}

解释下:

我初始化了一个定长 map 类型,然后我起了两个协程,一个去赋值,一个去取值。

看似没什么问题,但是你执行下,就会发现,他崩溃了!

代码语言:javascript
复制
$ go run map.go 
fatal error: concurrent map read and map write

goroutine 18 [running]:
runtime.throw({0x10648e9, 0x0})
        /usr/local/go/src/runtime/panic.go:1198 +0x71 fp=0xc00002af90 sp=0xc00002af60 pc=0x102b9d1
runtime.mapaccess1_fast64(0x0, 0x0, 0x2)
        /usr/local/go/src/runtime/map_fast64.go:21 +0x172 fp=0xc00002afb0 sp=0xc00002af90 pc=0x100d672
main.main.func2()
        /Users/kun/Desktop/课件/bingfa-demo/map.go:14 +0x2e fp=0xc00002afe0 sp=0xc00002afb0 pc=0x10556ce
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1581 +0x1 fp=0xc00002afe8 sp=0xc00002afe0 pc=0x1052921
created by main.main
        /Users/kun/Desktop/课件/bingfa-demo/map.go:12 +0x97

goroutine 1 [select (no cases)]:
main.main()
        /Users/kun/Desktop/课件/bingfa-demo/map.go:17 +0x9c

goroutine 17 [runnable]:
main.main.func1()
        /Users/kun/Desktop/课件/bingfa-demo/map.go:8 +0x35
created by main.main
        /Users/kun/Desktop/课件/bingfa-demo/map.go:6 +0x65
exit status 2

因为 Go 内建的 map 对象并不是线程安全的,在并发读写时他会进行检查,就会出现错误!

但是庆幸的是,这个报错相对来说还是比较好定位的,通过错误提示基本都能快速找到报错点!

为 Map 加读写锁,保证线程安全

为了解决并发情况下 map 的操作,我们必须得自己去实现一个线程安全的结构体!

这里提供一个范例:

代码语言:javascript
复制
// RWMap 一个读写锁保护的线程安全的map
type RWMap struct { 
 sync.RWMutex // 读写锁保护下面的map字段
 m map[int]int
}
// NewRWMap 新建一个RWMap
func NewRWMap(n int) *RWMap {
 return &RWMap{
  m: make(map[int]int, n),
 }
}
// Get 获取值
func (m *RWMap) Get(k int) (int, bool) {
 m.RLock()
 defer m.RUnlock()
 v, existed := m.m[k] // 在锁的保护下从map中读取
 return v, existed
}
// Set 写值
func (m *RWMap) Set(k int, v int) { 
 m.Lock()              // 锁保护
 defer m.Unlock()
 m.m[k] = v
}
// Delete 删除值
func (m *RWMap) Delete(k int) { //删除一个键
 m.Lock()                   // 锁保护
 defer m.Unlock()
 delete(m.m, k)
}
// Len 获取长度
func (m *RWMap) Len() int { // map的长度
 m.RLock()   // 锁保护
 defer m.RUnlock()
 return len(m.m)
}
// Each 循环遍历
func (m *RWMap) Each(f func(k, v int) bool) { // 遍历map
 m.RLock()             //遍历期间一直持有读锁
 defer m.RUnlock()
 for k, v := range m.m {
  if !f(k, v) {
   return
  }
 }
}

因为 Go 目前为止并不支持泛型,所以我们并不能实现一个通用的加锁 map。

当然我们也可以通过 interface{} 来模拟泛型,但是成本太高,性能也会大打折扣。

我们一般在开发中,对 map 对象的操作无非就是 增删改查和遍历等几种操作。

我们在并发时,只需要对他进行加读写锁,就能防止 map 并发读写报的问题!

写在最后的话

虽然读写锁可以提供一个线程安全的 map,但是在大量并发情况下,锁的竞争会非常激烈,于是也就有了 锁是性能下降的万恶之源 的说法。

于是我们在并发编程中的原则就是:尽量少的使用锁,如果要用,尽量做到减少锁的粒度和锁的持有时间

基于这个思路,线程安全的 map 除了我们刚才使用的加读写锁的思路,还有分片加锁,sync.Map方案,感兴趣的可以去查阅相关的资料。

这里就不展开讲解了,因为一般我们对性能追求不是追求那么的极致,上读写锁就基本够用了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GoLang全栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用Map常见的2种错误
    • 错误一:未初始化
      • 错误二:并发读写
      • 为 Map 加读写锁,保证线程安全
      • 写在最后的话
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档