前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试官:map为什么是非线程安全的?

面试官:map为什么是非线程安全的?

作者头像
小锟哥哥
发布2022-05-10 09:10:53
1.5K0
发布2022-05-10 09:10:53
举报
文章被收录于专栏:GoLang全栈

go 语言中 map 默认是并发不安全的。

为什么要这么设计呢?

这就是矛与盾的关系,go 语言的设计者认为,在大部分场景中,对 map 的操作都非线程安全的;

我们不可能为了那小部分的需求,而牺牲大部分人的性能。

所以如果我们要使用线程安全的 map 的话,就需要做一些调整了。

那 go 语言中我们要使用线程安全的 map,该怎么操作呢?

非线程安全 map 的 panic

首先我们先来看下,map 如果不注意线程安全会报什么错!

直接上一个代码:

代码语言:javascript
复制
package main

import (
    "fmt"
    "time"
)

func main() {
    s := make(map[int]int)
  // 启 100 个异步线程去写 map
    for i := 0; i < 100; i++ {
        go func(i int) {
            s[i] = i
        }(i)
    }
  // 启 100 个一步线程去读 map
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Printf("map 第 %d 个元素值是 %d", i, s[i])
        }(i)
    }
  // 睡眠 3 秒钟,方便前面的协程打印
    time.Sleep(3 * time.Second)
}

这里我瞬间起了 100 个 协程去写值,然后又瞬间起 100 个协程去读里面的值。

因为是异步协程,所以 map 同一时刻就可能被多个写的协程操作。

那么运行后就会报错:fatal error: concurrent map writes

方式一、加读写锁

要解决线程安全问题,第一个方案就是给他家读写锁。

读取的时候加读锁,写的时候加写锁。

当然如果你想加互斥锁也是可以的,只要上锁就好了。

这里提供一个 demo 代码:

代码语言:javascript
复制
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var lock sync.RWMutex
    s := make(map[int]int)
    for i := 0; i < 100; i++ {
        go func(i int) {
            lock.Lock()
            s[i] = i
            lock.Unlock()
        }(i)
    }
    for i := 0; i < 100; i++ {
        go func(i int) {
            lock.RLock()
            fmt.Printf("map 第 %d 个元素值是 %d", i, s[i])
            lock.RUnlock()
        }(i)
    }
  // 睡眠 3 秒钟,方便前面的协程打印
    time.Sleep(3 * time.Second)
}

方式二、使用官方的 sync.Map

go 语言官方其实也为我们提供了一个线程安全的 map,就在 sync 包里面。

用他里面的 map 也是线程安全的。

这个 map 使用起来,就没有基础的 map 方便了,写值的时候得通过 Store 方法,读的时候使用方法 Load 来读取。

当然还有其他API,我放一个截图:

下面我也提供一个使用 demo 代码:

代码语言:javascript
复制
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var m sync.Map
    for i := 0; i < 100; i++ {
        go func(i int) {
            m.Store(i, i)
        }(i)
    }
    for i := 0; i < 100; i++ {
        go func(i int) {
            v, ok := m.Load(i)
            fmt.Printf("Load: %v, %v", v, ok)
        }(i)
    }
  // 睡眠 3 秒钟,方便前面的协程打印
    time.Sleep(3 * time.Second)
}

两个方案的差异

我们先来看下 sync.Map 的结构体:

代码语言:javascript
复制
type Map struct {
   mu Mutex
   read atomic.Value // readOnly
   dirty map[interface{}]*entry
   misses int
}

Go 语言的 sync.Map 支持并发读写,采取了 “空间换时间” 的机制,冗余了两个数据结构,分别是:read 和 dirty。

和我们的第一种方案 map+RWMutex 的实现并发的方式相比,减少了加锁对性能的影响。

它做了一些优化:可以无锁访问read map,而且会优先操作read map,倘若只操作read map就可以满足要求,那就不用去操作write map(dirty)。

所以在某些特定场景中它发生锁竞争的频率会远远小于 map+RWMutex 的实现方式

这样做的优点是:适合读多写少的场景;

缺点也就是:如果是写多的场景,会导致 read map 缓存失效,需要加锁,冲突变多,性能急剧下降。

你学废了么?

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 非线程安全 map 的 panic
  • 方式一、加读写锁
  • 方式二、使用官方的 sync.Map
  • 两个方案的差异
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档