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 ...
随机推荐
- 基于.Net开发的、支持多平台、多语言餐厅点餐系统
今天给大家推荐一套支持多平台.多语言版本的订单系统,适合餐厅.酒店等场景. 项目简介 这是基于.Net Framework开发的,支持手机.平板.PC等平台.多语言版本开源的点餐系统,非常适合餐厅.便 ...
- Java面试——Spring
一.Spring Bean 作用域 [1]singleton:该属性在 IOC容器仅创建一个 Bean实例(单例),IOC容器每次返回的是同一个 Bean实例.[2]prototype:该属性在 IO ...
- 记一次 .NET 某医疗住院系统 崩溃分析
一:背景 1. 讲故事 最近收到了两起程序崩溃的dump,查了下都是经典的 double free 造成的,蛮有意思,这里就抽一篇出来分享一下经验供后面的学习者避坑吧. 二:WinDbg 分析 1. ...
- Windows10一劳永逸的禁止更新/恢复更新
之前发表过一篇文章Windows10彻底关闭自动更新,这篇文章相对复杂了些.而且还是有一定几率会触发从而自动打开更新.下面讲的就是怎么一次性永久关闭更新,即使触发了更新,也不能下载更新,从而达到真正的 ...
- 普冉PY32系列(七) SOP8, SOP10和SOP16封装的PY32F003/PY32F002A管脚复用
目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...
- 让代码变得优雅简洁的神器:Java8 Stream流式编程
原创/朱季谦 本文主要基于实际项目常用的Stream Api流式处理总结. 因笔者主要从事风控反欺诈相关工作,故而此文使用比较熟悉的三要素之一的[手机号]黑名单作代码案例说明. 我在项目当中,很早就开 ...
- 【Python爬虫(一)】XPath
解析方式:XPath XPath的基本使用 1 安装lxml库 conda install lxml 下载慢的话可以试一下热点或切换下载源 2 导入etree from lxml import etr ...
- 在.NET 6.0中自定义接口路由
大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进. 在本文中,我们将讨论ASP.NET Core中的新路由.我们将了解什么是接口(en ...
- 因果推断-Caual Inference
两种形式 Reduced Form:Let data speak itself,主要采用regression等方法 Structure Approach:Data only can never rev ...
- [白嫖]阿里云服务器ECS以及无影云桌面
[白嫖]aliyun 阿里云ECS和无影云桌面 [云服务器 ECS]学生认证约7个月 适合入门级开发者,用于搭建网站.小程序.云博客.部署主流开发环境,如 lamp.node.js.wordpress ...