原文链接: 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 的并发安全性,从而在性能和安全性之间做出权衡。

如何并发安全

接下来介绍三种并发安全的方式:

  1. 读写锁
  2. 分片加锁
  3. 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

定义了两个方法:ReadMapWriteMap。在 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 的锁数组。

定义了两个方法:ReadMapWriteMap。在这两个方法中,我们都使用了一个 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 语言 map 是并发安全的吗?的更多相关文章

  1. go语言学习--map的并发

    go提供了一种叫map的数据结构,可以翻译成映射,对应于其他语言的字典.哈希表.借助map,可以定义一个键和值,然后可以从map中获取.设置和删除这个值,尤其适合数据查找的场景.但是map的使用有一定 ...

  2. go语言坑之并发访问map

    fatal error: concurrent map read and map write 并发访问map是不安全的,会出现未定义行为,导致程序退出.所以如果希望在多协程中并发访问map,必须提供某 ...

  3. Go语言基础之并发

    并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因. Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天) ...

  4. GO学习-(18) Go语言基础之并发

    Go语言基础之并发 并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因. Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(你在用微 ...

  5. Golang语言系列-11-goroutine并发

    goroutine 并发 概念 package main import ( "fmt" "time" ) /* [Go语言中的并发编程 goroutine] [ ...

  6. Go语言中的并发编程

    并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因. Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天) ...

  7. Go语言系列之并发编程

    Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(宏观上并行,微观上并发). 并行:同一时刻执行多个任务(宏观和微观都是并行). Go语言的并发通过goroutine实现.gorout ...

  8. Go语言Map的使用

    Go 语言Map(集合) Map 是一种无序的键值对的集合.Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值. Map 是一种集合,所以我们可以像迭代数组和切片那样 ...

  9. go语言---map

    go语言---map https://blog.csdn.net/cyk2396/article/details/78890185 一.map的用法: type PersonDB struct { I ...

  10. go语言-csp模型-并发通道

    [前言]go语言的并发机制以及它所使用的CSP并发模型 一.CSP并发模型 CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型. C ...

随机推荐

  1. 声网 Agora 音频互动 MoS 分方法:为音频互动体验进行实时打分

    在业界,实时音视频的 QoE(Quality of Experience) 方法一直都是个重要的话题,每年 RTE 实时互联网大会都会有议题涉及.之所以这么重要,其实是因为目前 RTE 行业中还没有一 ...

  2. springboot 接入 ChatGPT

    项目地址 https://gitee.com/Kindear/lucy-chat 介绍 lucy-chat是接入OpenAI-ChatGPT大模型人工智能的Java解决方案,大模型人工智能的发展是不可 ...

  3. 解决margin合并问题

    一.什么是外边距合并 外边距合并(叠加)是一个相当简单的概念.但是,在实践中对网页进行布局时,它会造成许多混淆. 所谓的外边距合并就是,当两个垂直外边距相遇时,它们将形成一个外边距.合并的外边距的高度 ...

  4. vite项目生产环境去掉console信息【转载】

    环境变量引入 通常去掉console为生产环境,即需要引入环境变量.具体请看这篇文章: vite项目初始化之~环境变量 注意 与webpacak相比,vite已经将这个功能内置到了,所以我们只需要配置 ...

  5. Apinto Dashboad V2.0 发布:可视化控制台让配置更轻松!

    大家好, Eolink 旗下开源网关 Apinto 本次带来了 Apinto Dashboad V2.0 的版本发布. Dashboad 需要与 Apinto 主版本一起使用,目前 Dashboad ...

  6. Teamcenter_NX集成开发:UF_UGMGR_invoke_pdm_server函数的使用

    之前了解到通过UFUN函数UF_UGMGR_invoke_pdm_server可以调用Teamcenter ITK函数,从而可以获取及编辑Teamcenter对象.UFUN中有样例代码,但是就是不知道 ...

  7. c#动态执行字符串脚本(优化版)

    像javascript中有eval()来执行动态代码,c#中是没有的,于是自己动手丰衣足食, 先来代码 1 using System; 2 using System.Data; 3 using Sys ...

  8. Redhat7/CentOS7 网络配置与管理(nmtui、nmcli、GNOME GUI、ifcfg文件、IP命令)

    Redhat7/CentOS7 网络配置与管理(nmtui.nmcli.GNOME GUI.ifcfg文件.IP命令) 背景:作为系统管理员,需要经常处理主机网络问题,而配置与管理网络的方法和工具也有 ...

  9. 四月九号java知识

    1.do{}while();和while(){}结构最主要区别就是前者后面要一个分号 2.System.out.print();与System.out.println();的区别后者输出换行, 前者不 ...

  10. Redis读书笔记(三)

    单机数据库的实现 Redis数据库 Redis数据库的实现 struct redisServer { //... //保存服务器中的所有数据库, 数组 redisDB *db; //服务器的数据库数量 ...