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

ops/2025/1/14 23:40:30/

本文详细介绍Golang常用数据类型的并发安全性,特别是复合数据类型(数组、Slice、Map、Channel)的并发安全性。

在这里插入图片描述

文章目录

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

线程安全(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 执行,避免多次关闭或不平衡的生产者/消费者造成死锁。

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


http://www.ppmy.cn/ops/150135.html

相关文章

ExplaineR:集成K-means聚类算法的SHAP可解释性分析 | 可视化混淆矩阵、决策曲线、模型评估与各类SHAP图

集成K-means聚类算法的SHAP可解释性分析 加载数据集并训练机器学习模型 SHAP 分析以提取特征对预测的影响 通过混淆矩阵可视化模型性能 决策曲线分析 模型评估&#xff08;多指标和ROC曲线的目视检查&#xff09; 带注释阈值的 ROC 曲线 加载 SHAP 结果以进行下游分析 与…

MySQL 最新数据库查询指南

MySQL 最新数据库查询指南 1. 引言 MySQL 是目前广泛使用的关系型数据库管理系统&#xff0c;其强大的查询功能让开发者能够高效地获取和操作数据。在这篇博客中&#xff0c;我们将深入探讨 MySQL 的查询功能&#xff0c;包括单表查询、多表查询、子查询、聚合查询、以及最新…

编译与汇编

本文来自《程序员的自我修养》 编译过程是把预处理完的文件进行一系列词法分析&#xff0c;语法分析&#xff0c;语义分析以及优化后生成相应的汇编文件代码。 现在版本的GCC把预编译和编译两个步骤合并为一个步骤。 gcc -S HelloWorld.c HelloWorld.sint main() {//test/* …

前端开发:字体属性

1.设置字体家族 也就是使用宋体还是微软雅黑 2.设置字体大小 3.设置字体颜色 4.设置字体粗细 5.设置文字样式 <style> div{ font-family: 宋体; //设置字体家族 font-size:50px; //设置字体大小 color:red; //设置字体颜色&#xff1b…

如何理解机器学习中的向量 ?

在机器学习中&#xff0c;向量是一个核心概念&#xff0c;用于表示数据、特征或其他数学结构。向量是线性代数的基本对象&#xff0c;在机器学习中&#xff0c;它的意义超越了纯数学的定义&#xff0c;承载着信息的表示、计算和模型学习等重要功能。以下是对机器学习中向量的详…

合洁科技:晶圆洁净车间的净化空调系统和一般空调系统有何区别

电子洁净车间的净化空调系统和一般空调系统之间存在显著差异&#xff0c;这些差异主要体现在空气处理过程、空气过滤要求、室内压力控制、气流组织、换气次数以及能耗方面。以下是具体的对比&#xff0c;随合洁科技电子净化工程公司一起来了解下吧! 一、空气处理过程 净化空调系…

精选2款.NET开源的博客系统

前言 博客系统是一个便于用户创建、管理和分享博客内容的在线平台&#xff0c;今天大姚给大家分享2款.NET开源的博客系统。 StarBlog StarBlog是一个支持Markdown导入的开源博客系统&#xff0c;后端基于最新的.Net6和Asp.Net Core框架&#xff0c;遵循RESTFul接口规范&…

字符串算法篇——字里乾坤,算法织梦,解构字符串的艺术(上)

文章目录 引言&#xff1a;一场字符与算法的交响曲第一章&#xff1a;从匹配到理解——字符串的基础算法1.1 暴力搜索&#xff1a;逐字逐句的匠人精神1.2 KMP算法&#xff1a;文字间的优雅跳跃 第二章&#xff1a;字符串的变形之术——编辑与构造第三章&#xff1a;应用与未来—…