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

server/2025/1/15 23:48:01/

大家好,这里是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/server/158688.html

相关文章

【Artificial Intelligence篇】AI 入侵家庭:解锁智能生活的魔法密码,开启居家梦幻新体验

家庭智能化的时代已经到来&#xff0c;准备好了嘛&#xff01;&#xff01;&#xff01; 在当今数字化浪潮汹涌澎湃的时代&#xff0c;人工智能&#xff08;AI&#xff09;宛如一位神秘而强大的魔法师&#xff0c;悄然 “入侵” 了我…

CSS | CSS实现两栏布局(左边定宽 右边自适应,左右成比自适应)

目录 一、左边定宽 右边自适应 1.浮动 2.利用浮动margin 3.定位margin 4.flex布局 5.table 布局 二、左右成比自适应 1:1 1flex布局 table布局 1:2 flex布局 三列布局链接&#xff1a;CSS | 实现三列布局&#xff08;两边边定宽 中间自适应&#xff0c;自适应成比&#xff09;-…

Excel多层嵌套IF条件写法

Excel多层嵌套IF条件的实现方法 需求如下 利润 > 35% 卖价 成本 *&#xff08;1-毛利0.15&#xff09;利润 < 35% 并 >0.34 卖价 成本 *&#xff08;1-毛利0.14&#xff09;利润 < 34% 并 >0.33 卖价 成本 *&#xff08;1-毛利0.13&#xff09;利润 < 33% …

【Vim Masterclass 笔记09】S06L22:Vim 核心操作训练之 —— 文本的搜索、查找与替换操作(第一部分)

文章目录 S06L22 Search, Find, and Replace - Part One1 从光标位置起&#xff0c;正向定位到当前行的首个字符 b2 从光标位置起&#xff0c;反向查找某个字符3 重复上一次字符查找操作4 定位到目标字符的前一个字符5 单字符查找与 Vim 命令的组合6 跨行查找某字符串7 Vim 的增…

Golang|单机并发缓存

var m sync.Mutex //sync.Mutex 是一个互斥锁&#xff0c;可以由不同的协程加锁和解锁。 //sync.Mutex 是 Go 语言标准库提供的一个互斥锁 //当一个协程(goroutine)获得了这个锁的拥有权后&#xff0c;其它请求锁的协程(goroutine)就会阻塞在 Lock() 方法的调用上&#xff0c;直…

日常网络小知识

日常网络小知识 IPV4 IPv4&#xff08;Internet Protocol version 4&#xff09;是最常用的互联网协议之一&#xff0c;它使用 32 位二进制数来表示一个 IP 地址&#xff0c;通常写成十进制形式&#xff0c;用点分隔四个数字&#xff0c;每个数字表示一个字节&#xff08;8位…

Deep4SNet: deep learning for fake speech classification

Deep4SNet&#xff1a;用于虚假语音分类的深度学习 摘要&#xff1a; 虚假语音是指即使通过人工智能或信号处理技术产生的语音记录。生成虚假录音的方法有"深度语音"和"模仿"。在《深沉的声音》中&#xff0c;录音听起来有点合成&#xff0c;而在《模仿》中…

力扣经典题目之55.跳跃游戏

2&#xff0c;解题思路 public class Solution {public boolean canJump(int[] nums) {int n nums.length;int rightmost 0;for (int i 0; i < n; i) {if (i < rightmost) {rightmost Math.max(rightmost, i nums[i]);if (rightmost > n - 1) {return true;}}}r…