Golang笔记——数组、Slice、Map、Channel的并发安全性

devtools/2025/1/15 15:36:07/

大家好,这里是Good Note,关注 公主号:Goodnote,专栏文章私信限时Free。本文详细介绍Golang常用数据类型的并发安全性,特别是复合数据类型(数组、Slice、Map、Channel)的并发安全性。

在这里插入图片描述

文章目录

    • 线程安全(Thread Safety)定义
    • 1. 数组
      • 非并发安全性
      • 高并发下的使用注意
      • 示例
        • 并发写入数组(需加锁)
    • 2. 切片
      • 非并发安全性
      • 高并发下的使用注意
      • 示例
        • 并发写入切片(需加锁)
        • 错误示例:并发读写切片
    • 3. Map
      • 非并发安全性
      • 高并发下的使用注意
      • 示例
        • 错误示例
        • 正确用法 1:使用 `sync.Map`
        • 正确用法 2:使用 `sync.Mutex`
    • 4. Channel
      • 并发安全性
      • 高并发下的使用注意
      • 示例
        • 正确用法
        • 错误示例:多次关闭 `channel`
    • 总结
    • 历史文章
      • MySQL数据库
      • Redis
      • Golang

线程安全(Thread Safety)定义

线程安全(Thread Safety) 是指在多线程(或多 Goroutine)环境下,多个线程或 Goroutine 同时访问共享数据时,不会导致数据不一致或程序出现异常行为的特性


1. 数组

非并发安全性

  • 不线程安全:数组是 Go 的值类型,对同一个数组的并发访问(读或写)会导致数据竞争。
  • 特点
    1. 多个 Goroutine 对同一个数组同时进行写操作可能会破坏数据一致性
    2. 如果仅进行并发读操作,数组是线程安全的

高并发下的使用注意

  1. 对数组的读写操作需要通过锁(如 sync.Mutexsync.RWMutex)或其他同步机制来保护。
  2. 如果对数组进行复制后再操作,副本是独立的,可以避免并发问题。
  3. 在高并发场景中,直接使用数组的性能和扩展性较差,建议使用切片或其他并发友好的数据结构。
  1. 数组的固定大小和值类型特性导致性能和扩展性较差,需要显式的锁机制来确保线程安全。
  2. 切片通过动态扩容和引用传递解决了数组的局限性,并提供更高的灵活性和更低的内存开销,因此更适合高并发场景。

示例

并发写入数组(需加锁)
package mainimport ("fmt""sync"
)func main() {var arr = [5]int{}var mu sync.Mutexvar wg sync.WaitGroupfor i := 0; i < len(arr); i++ {wg.Add(1)go func(index int) {defer wg.Done()mu.Lock()arr[index] = index * 10mu.Unlock()}(i)}wg.Wait()fmt.Println(arr)
}

2. 切片

非并发安全性

  • 不线程安全:切片是对底层数组的引用。高并发下对同一个切片的操作(如扩容、写入等)会导致数据竞争或崩溃。
  • 问题来源
    1. 切片的长度和容量在运行时动态变化,扩容操作会重新分配底层数组,并可能导致未同步的 Goroutine 操作过期引用。
    2. 并发写入同时进行读写会破坏底层数据的一致性。

高并发下的使用注意

  1. 读写分离
    • 如果多个 Goroutine 并发读切片,而没有写入操作,则是线程安全的。
    • 一旦存在写操作,需要使用同步机制(如 sync.Mutexsync.RWMutex)保护。
  2. 扩容风险
    • 切片扩容时会重新分配底层数组,建议在并发环境中提前设置好切片的容量,避免动态扩容带来的问题

示例

并发写入切片(需加锁)
package mainimport ("fmt""sync"
)func main() {slice := []int{}var mu sync.Mutexvar wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func(val int) {defer wg.Done()mu.Lock()slice = append(slice, val)mu.Unlock()}(i)}wg.Wait()fmt.Println(slice)
}

输出:

  • 解释:并发写入切片的操作使用了 sync.Mutex,这确保了同一时刻只有一个 Goroutine 能访问切片并进行 append 操作,因此数据不会发生竞争,程序不会崩溃。
  • 输出的顺序slice 最终会包含从 0 到 9 的所有数字,但由于并发执行的特性,数字的顺序通常是无序的。这是因为虽然对切片的写操作是互斥的,但 Goroutine 执行的顺序依然是不可预测的,所以 append 操作的顺序也无法保证。

如果希望按照顺序(0-9)输出,可以通过使用 channel 来保证按顺序收集并输出结果,或使用其他同步方式如 sync.Once 来协调。这属于Go协程同步的问题。

错误示例:并发读写切片
package mainimport ("fmt""sync"
)func main() {var wg sync.WaitGroupslice := []int{}for i := 0; i < 10; i++ {wg.Add(1) // 为每个 Goroutine 添加一个计数go func(val int) {defer wg.Done()            // 每个 Goroutine 执行完成后,计数器减一slice = append(slice, val) // 并发写入切片}(i)}wg.Wait()          // 等待所有 Goroutine 执行完毕fmt.Println(slice) // 输出结果
}

输出:

  • 输出个数不确定:因为并发写入时发生了数据竞争切片在扩容时可能丢失元素。
  • append 会在切片的底层数组容量不足时进行扩容,这个扩容过程是 不安全的,并且切片的底层数组是指向连续内存空间的。如果多个 Goroutine 同时执行 append 操作并且触发了扩容,就可能导致数据丢失或不可预期的行为。

3. Map

非并发安全性

  • 不线程安全:Go 的原生 map 类型在并发访问时不安全,直接进行并发读写会导致数据竞争或运行时崩溃(fatal error: concurrent map read and map write)。
  • 原因
    1. map 的底层实现没有锁机制,多个 Goroutine 同时修改 map 的内部结构会破坏其一致性。
    2. 读写操作之间没有同步机制,可能导致部分数据状态不一致。

高并发下的使用注意

  1. 使用 sync.Map
    • sync.Map 是线程安全的 map,支持高并发场景。
    • 缺点:性能可能低于加锁的原生 map,尤其在频繁读写情况下。
  2. 使用 锁机制
    • 使用 sync.Mutexsync.RWMutex 对原生 map 加锁。

示例

错误示例
package mainimport ("fmt""sync"
)func main() {m := make(map[int]int)var wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func(val int) {defer wg.Done()m[val] = val}(i)}wg.Wait()fmt.Println(m)
}

输出:fatal error: concurrent map writes

正确用法 1:使用 sync.Map
package mainimport ("fmt""sync"
)func main() {var m sync.Mapvar wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func(val int) {defer wg.Done()m.Store(val, val) // 并发安全的写入}(i)}wg.Wait()// 遍历 sync.Mapm.Range(func(key, value any) bool {fmt.Printf("key: %v, value: %v\n", key, value)return true})
}

输出示例:

key: 0, value: 0
key: 9, value: 9
key: 7, value: 7
key: 3, value: 3
key: 5, value: 5
key: 2, value: 2
key: 6, value: 6
key: 8, value: 8
key: 4, value: 4
key: 1, value: 1
正确用法 2:使用 sync.Mutex
package mainimport ("fmt""sync"
)func main() {m := make(map[int]int)var mu sync.Mutexvar wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func(val int) {defer wg.Done()mu.Lock()m[val] = valmu.Unlock()}(i)}wg.Wait()fmt.Println(m)
}

输出示例:

map[0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9]

4. Channel

并发安全性

channel的源码中使用了互斥锁,不允许并发读写。详细参考:Go语言面试之——channel

  • 线程安全:Go 的 channel 是线程安全的,支持在多个 Goroutine 中并发进行读写操作。
  • 注意事项
    1. 关闭 channel
      • 关闭一个 channel 必须由单个 Goroutine 执行,多个 Goroutine 并发关闭会导致运行时错误(panic: close of closed channel)。
    2. 读写阻塞
      • 如果没有消费者读取数据,发送到 channel 的数据会阻塞发送 Goroutine。
      • 如果没有生产者发送数据,读取 channel 会阻塞接收 Goroutine。

高并发下的使用注意

  1. 保证关闭操作由单一 Goroutine 处理
  2. 根据需求设置缓冲区大小
    • 对于高并发场景,可以使用带缓冲的 channel 来提高性能。
  3. 避免死锁
    • 设计时确保 channel 的生产者和消费者平衡。

示例

正确用法
package mainimport ("fmt""sync"
)func main() {ch := make(chan int, 5)var wg sync.WaitGroup// 并发写入for i := 0; i < 5; i++ {wg.Add(1)go func(val int) {defer wg.Done()ch <- val}(i)}// 关闭通道go func() {wg.Wait()close(ch) // 单一 Goroutine 负责关闭}()// 读取数据for val := range ch {fmt.Println(val)}
}
错误示例:多次关闭 channel
package mainfunc main() {ch := make(chan int)go func() {close(ch) // 第一次关闭}()go func() {close(ch) // 第二次关闭,导致 panic}()select {}
}

输出异常:panic: close of closed channel


总结

数据结构是否线程安全高并发注意事项
数组需要加锁保护访问;在并发场景中,数组性能较差,建议使用切片或其他更灵活的结构。
切片切片是引用类型,需要加锁保护并发访问,尤其是扩容场景;多读无写时是线程安全的。
Map使用 sync.Map 或加锁实现并发安全;直接并发读写会导致数据竞争或崩溃。
Channel本身线程安全;但关闭操作需由单一 Goroutine 执行,避免多次关闭或不平衡的生产者/消费者造成死锁。

通过合理的设计和同步机制,可以在高并发场景中安全地使用这些数据结构。

历史文章

MySQL数据库

MySQL数据库

Redis

Redis数据库笔记合集

Golang

  1. Golang笔记——语言基础知识
  2. Golang笔记——切片与数组
  3. Golang笔记——hashmap
  4. Golang笔记——rune和byte
  5. Golang笔记——channel
  6. Golang笔记——Interface类型

http://www.ppmy.cn/devtools/150325.html

相关文章

Linux(18)——提高命令行运行效率

目录 一、创建和执行 shell 脚本&#xff1a; 1、命令解释器&#xff1a; 2、执行 Bash Shell 脚本&#xff1a; 3、从 shell 脚本提供输出&#xff1a; 二、对特殊字符加引号&#xff1a; 1、反斜杠 &#xff08;\&#xff09;&#xff1a; 2、单引号 &#xff08; &…

从 MySQL 到 ClickHouse 的迁移与优化——支持上亿级数据量的复杂检索

文章目录 1. ClickHouse 背景与使用场景1.1 ClickHouse 简介1.2 ClickHouse 的特点1.3 ClickHouse 的使用场景 2. 从 MySQL 到 ClickHouse 的迁移2.1 MySQL 与 ClickHouse 的对比2.2 迁移背景2.3 迁移注意事项2.3.1 数据模型设计2.3.2 数据迁移2.3.3 SpringBoot 项目改造2.3.4 …

互联网架构变迁:从 TCP/IP “呼叫” 到 NDN “内容分发” 的逐浪之旅

本文将给出关于互联网架构演进的一个不同视角。回顾一下互联网的核心理论基础产生的背景&#xff1a; 左边是典型的集中控制通信网络&#xff0c;很容易被摧毁&#xff0c;而右边的网络则没有单点问题&#xff0c;换句话说它很难被全部摧毁&#xff0c;与此同时&#xff0c;分…

设计模式——单例模式

单例模式 实现单例模式的方法前置条件懒汉式&#xff08;Lazy Initialization&#xff09;饿汉式&#xff08;Eager Initialization&#xff09;双重锁式&#xff08;Double-Checked Locking&#xff09;静态内部类式&#xff08;Static Inner Class&#xff09;枚举式&#xf…

Spring——自动装配

假设一个场景&#xff1a; 一个人&#xff08;Person&#xff09;有一条狗&#xff08;Dog&#xff09;和一只猫(Cat)&#xff0c;狗和猫都会叫&#xff0c;狗叫是“汪汪”&#xff0c;猫叫是“喵喵”&#xff0c;同时人还有一个自己的名字。 将上述场景 抽象出三个实体类&…

Git使用笔记

Git 版本控制 一、Git 介绍二、Git 使用1. 安装及配置2. 使用方法3. Git 命令3. 历史版本回退4. 分支 (Branch) 三、远程仓库1. SSH公钥连接Gitee2. 推送到远程仓库 一、Git 介绍 常见版本控制软件&#xff1a;集中式&#xff08;CVS、SVN&#xff09;&#xff0c;分布式&#…

CentOS7安装redis

CentOS7安装redis 文章目录 CentOS7安装redis1、更换阿里云镜像2、安装gcc语言环境3、安装epel仓库4、安装redis5、启动redis6、验证redis7、可视化工具连接8、参考文章 1、更换阿里云镜像 使用yum安装不成功可能是镜像源有问题建议替换成阿里云的 失败事例 备份已有镜像 将…

UI自动化测试保姆级教程--pytest详解(精简易懂)

欢迎来到啊妮莫的学习小屋 别让过去的悲伤&#xff0c;毁掉当下的快乐一《借东西的小人阿莉埃蒂》 简介 pytest是一个用于Python的测试框架, 支持简单的单元测试和复杂的功能测试. 和Python自带的UnitTest框架类似, 但是相比于UnitTest更加简洁, 效率更高. 特点 非常容易上手…