Go 语言 map 是并发安全的吗?
原文链接: Go 语言 map 是并发安全的吗?
Go 语言中的 map 是一个非常常用的数据结构,它允许我们快速地存储和检索键值对。然而,在并发场景下使用 map 时,还是有一些问题需要注意的。
本文将探讨 Go 语言中的 map 是否是并发安全的,并提供三种方案来解决并发问题。
先来回答一下题目的问题,答案就是并发不安全。
看一段代码示例,当两个 goroutine 同时对同一个 map 进行写操作时,会发生什么?
package main
import "sync"
func main() {
m := make(map[string]int)
m["foo"] = 1
var wg sync.WaitGroup
wg.Add(2)
go func() {
for i := 0; i < 1000; i++ {
m["foo"]++
}
wg.Done()
}()
go func() {
for i := 0; i < 1000; i++ {
m["foo"]++
}
wg.Done()
}()
wg.Wait()
}
在这个例子中,我们可以看到,两个 goroutine 将尝试同时对 map 进行写入。运行这个程序时,我们将看到一个错误:
fatal error: concurrent map writes
也就是说,在并发场景下,这样操作 map 是不行的。
为什么是不安全的
因为它没有内置的锁机制来保护多个 goroutine 同时对其进行读写操作。
当多个 goroutine 同时对同一个 map 进行读写操作时,就会出现数据竞争和不一致的结果。
就像上例那样,当两个 goroutine 同时尝试更新同一个键值对时,最终的结果可能取决于哪个 goroutine 先完成了更新操作。这种不确定性可能会导致程序出现错误或崩溃。
Go 语言团队没有将 map 设计成并发安全的,是因为这样会增加程序的开销并降低性能。
如果 map 内置了锁机制,那么每次访问 map 时都需要进行加锁和解锁操作,这会增加程序的运行时间并降低性能。
此外,并不是所有的程序都需要在并发场景下使用 map,因此将锁机制内置到 map 中会对那些不需要并发安全的程序造成不必要的开销。
在实际使用过程中,开发人员可以根据程序的需求来选择是否需要保证 map 的并发安全性,从而在性能和安全性之间做出权衡。
如何并发安全
接下来介绍三种并发安全的方式:
- 读写锁
- 分片加锁
- sync.Map
加读写锁
第一种方法是使用读写锁,这是最容易想到的一种方式。在读操作时加读锁,在写操作时加写锁。
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
sync.RWMutex
Map map[string]string
}
func NewSafeMap() *SafeMap {
sm := new(SafeMap)
sm.Map = make(map[string]string)
return sm
}
func (sm *SafeMap) ReadMap(key string) string {
sm.RLock()
value := sm.Map[key]
sm.RUnlock()
return value
}
func (sm *SafeMap) WriteMap(key string, value string) {
sm.Lock()
sm.Map[key] = value
sm.Unlock()
}
func main() {
safeMap := NewSafeMap()
var wg sync.WaitGroup
// 启动多个goroutine进行写操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
safeMap.WriteMap(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
}(i)
}
wg.Wait()
// 启动多个goroutine进行读操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d", i)))
}(i)
}
wg.Wait()
}
在这个示例中,我们定义了一个 SafeMap 结构体,它包含一个 sync.RWMutex 和一个 map[string]string。
定义了两个方法:ReadMap 和 WriteMap。在 ReadMap 方法中,我们使用读锁来保护对 map 的读取操作。在 WriteMap 方法中,我们使用写锁来保护对 map 的写入操作。
在 main 函数中,我们启动了多个 goroutine 来进行读写操作,这些操作都是安全的。
分片加锁
上例中通过对整个 map 加锁来实现需求,但相对来说,锁会大大降低程序的性能,那如何优化呢?其中一个优化思路就是降低锁的粒度,不对整个 map 进行加锁。
这种方法是分片加锁,将这个 map 分成 n 块,每个块之间的读写操作都互不干扰,从而降低冲突的可能性。
package main
import (
"fmt"
"sync"
)
const N = 16
type SafeMap struct {
maps [N]map[string]string
locks [N]sync.RWMutex
}
func NewSafeMap() *SafeMap {
sm := new(SafeMap)
for i := 0; i < N; i++ {
sm.maps[i] = make(map[string]string)
}
return sm
}
func (sm *SafeMap) ReadMap(key string) string {
index := hash(key) % N
sm.locks[index].RLock()
value := sm.maps[index][key]
sm.locks[index].RUnlock()
return value
}
func (sm *SafeMap) WriteMap(key string, value string) {
index := hash(key) % N
sm.locks[index].Lock()
sm.maps[index][key] = value
sm.locks[index].Unlock()
}
func hash(s string) int {
h := 0
for i := 0; i < len(s); i++ {
h = 31*h + int(s[i])
}
return h
}
func main() {
safeMap := NewSafeMap()
var wg sync.WaitGroup
// 启动多个goroutine进行写操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
safeMap.WriteMap(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
}(i)
}
wg.Wait()
// 启动多个goroutine进行读操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d", i)))
}(i)
}
wg.Wait()
}
在这个示例中,我们定义了一个 SafeMap 结构体,它包含一个长度为 N 的 map 数组和一个长度为 N 的锁数组。
定义了两个方法:ReadMap 和 WriteMap。在这两个方法中,我们都使用了一个 hash 函数来计算 key 应该存储在哪个 map 中。然后再对这个 map 进行读写操作。
在 main 函数中,我们启动了多个 goroutine 来进行读写操作,这些操作都是安全的。
有一个开源项目 orcaman/concurrent-map 就是通过这种思想来做的,感兴趣的同学可以看看。
sync.Map
最后,在内置的 sync 包中(Go 1.9+)也有一个线程安全的 map,通过将读写分离的方式实现了某些特定场景下的性能提升。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
// 启动多个goroutine进行写操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m.Store(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
}(i)
}
wg.Wait()
// 启动多个goroutine进行读操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
v, _ := m.Load(fmt.Sprintf("name%d", i))
fmt.Println(v.(string))
}(i)
}
wg.Wait()
}
有了官方的支持,代码瞬间少了很多,使用起来方便多了。
在这个示例中,我们使用了内置的 sync.Map 类型来存储键值对,使用 Store 方法来存储键值对,使用 Load 方法来获取键值对。
在 main 函数中,我们启动了多个 goroutine 来进行读写操作,这些操作都是安全的。
总结
Go 语言中的 map 本身并不是并发安全的。
在多个 goroutine 同时访问同一个 map 时,可能会出现并发不安全的现象。这是因为 Go 语言中的 map 并没有内置锁来保护对map的访问。
尽管如此,我们仍然可以使用一些方法来实现 map 的并发安全。
一种方法是使用读写锁,在读操作时加读锁,在写操作时加写锁。
另一种方法是分片加锁,将这个 map 分成 n 块,每个块之间的读写操作都互不干扰,从而降低冲突的可能性。
此外,在内置的 sync 包中(Go 1.9+)也有一个线程安全的 map,它通过将读写分离的方式实现了某些特定场景下的性能提升。
以上就是本文的全部内容,如果觉得还不错的话欢迎点赞,转发和关注,感谢支持。
参考文章:
推荐阅读:
- Go 语言切片是如何扩容的?
- Go 语言数组和切片的区别
- Go 语言 new 和 make 关键字的区别
- 为什么 Go 不支持 []T 转换为 []interface
- 为什么 Go 语言 struct 要使用 tags
Go 语言 map 是并发安全的吗?的更多相关文章
- go语言学习--map的并发
go提供了一种叫map的数据结构,可以翻译成映射,对应于其他语言的字典.哈希表.借助map,可以定义一个键和值,然后可以从map中获取.设置和删除这个值,尤其适合数据查找的场景.但是map的使用有一定 ...
- go语言坑之并发访问map
fatal error: concurrent map read and map write 并发访问map是不安全的,会出现未定义行为,导致程序退出.所以如果希望在多协程中并发访问map,必须提供某 ...
- Go语言基础之并发
并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因. Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天) ...
- GO学习-(18) Go语言基础之并发
Go语言基础之并发 并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因. Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(你在用微 ...
- Golang语言系列-11-goroutine并发
goroutine 并发 概念 package main import ( "fmt" "time" ) /* [Go语言中的并发编程 goroutine] [ ...
- Go语言中的并发编程
并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因. Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天) ...
- Go语言系列之并发编程
Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(宏观上并行,微观上并发). 并行:同一时刻执行多个任务(宏观和微观都是并行). Go语言的并发通过goroutine实现.gorout ...
- Go语言Map的使用
Go 语言Map(集合) Map 是一种无序的键值对的集合.Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值. Map 是一种集合,所以我们可以像迭代数组和切片那样 ...
- go语言---map
go语言---map https://blog.csdn.net/cyk2396/article/details/78890185 一.map的用法: type PersonDB struct { I ...
- go语言-csp模型-并发通道
[前言]go语言的并发机制以及它所使用的CSP并发模型 一.CSP并发模型 CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型. C ...
随机推荐
- 4.错误代码C1083
有的时候在VS中遇到的error C1083: 无法打开**: " * .*": No such file or directory的错误,这里总结了我遇到过的情况: 错误 C10 ...
- CSP2022-S游寄
游寄游寄,顾名思义,边游边寄 11.00AM 起床 复习了一下各种终端命令,然后又复习了一下对拍 虽然都没用到 然后接着睡. 有点小紧张,毕竟一年没搞OI 12.00AM 今天吃河虾 还行,只是有点扎 ...
- Firefox、Edge下无法使用jQuery的css("margin")、css("padding”)和css("border")获取值
今天遇到了浏览器的迷惑行为,在Edge上使用jQuery的css("margin")获取值,发现获取的是空值,换了Firefox也是如此.看了jquery官方原话,发现如下一段话R ...
- Kafka 物理存储机制
一个商业化消息队列的性能好坏,其文件存储机制设计是衡量一个消息队列服务技术水平和最关键指标之一.下面将从 Kafka文件存储机制和物理结构角度,分析 Kafka是如何实现高效文件存储,及实际应用效果. ...
- Unity C# IEnumrator 与 async 有的区别
前言 IEnumerator 和 async 是在 Unity 和 C# 中处理异步编程的两种不同方法.它们各自有不同的使用场景和优缺点. IEnumerator IEnumerator 是 C# 中 ...
- vue-i18n警告
vue3引入vue-i18n警告: bundler build of vue-i18n. It is recommended to configure your bundler to explicit ...
- ACM-CodeForces-#685(Div.2)
好久没见过CF有这么水的contest了,蒟蒻赶紧找找自信 A. Subtract or Divide #include<iostream> using namespace std; in ...
- '林子雨大数据' 实验3 HBase操作与接口编程
"林子雨大数据" 实验3 HBase操作与接口编程 环境搭建 VM虚拟机和Ubuntu系统的安装 在Windows中使用VirtualBox安装Ubuntu虚拟机(2020年7月版 ...
- 提交docker镜像
docker commit -m="提交的描述信息" -a="作者" 容器id 目标镜像名:[TAG]
- 淘宝/天猫获得淘宝商品评论 API 返回值说明
item_review-获得淘宝商品评论 taobao.item_review 公共参数 API测试工具 名称 类型 必须 描述 key String 是 调用key(必须以GET方式拼接在URL中) ...