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

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

文章目录

线程安全(Thread Safety)定义

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


1. 数组

非并发安全性

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

高并发下的使用注意

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

示例

并发写入数组(需加锁)
go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	var arr = [5]int{}
	var mu sync.Mutex
	var wg sync.WaitGroup

	for i := 0; i < len(arr); i++ {
		wg.Add(1)
		go func(index int) {
			defer wg.Done()
			mu.Lock()
			arr[index] = index * 10
			mu.Unlock()
		}(i)
	}

	wg.Wait()
	fmt.Println(arr)
}

2. 切片

非并发安全性

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

高并发下的使用注意

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

示例

并发写入切片(需加锁)
go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	slice := []int{}
	var mu sync.Mutex
	var wg sync.WaitGroup

	for 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协程同步的问题。

错误示例:并发读写切片
go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	slice := []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 main

import (
	"fmt"
	"sync"
)

func main() {
	m := make(map[int]int)
	var wg sync.WaitGroup

	for 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
go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(val int) {
			defer wg.Done()
			m.Store(val, val) // 并发安全的写入
		}(i)
	}

	wg.Wait()

	// 遍历 sync.Map
	m.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
go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	m := make(map[int]int)
	var mu sync.Mutex
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(val int) {
			defer wg.Done()
			mu.Lock()
			m[val] = val
			mu.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 的生产者和消费者平衡。

示例

正确用法
go 复制代码
package main

import (
	"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
go 复制代码
package main

func 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类型
相关推荐
StickToForever1 小时前
第4章 信息系统架构(五)
经验分享·笔记·学习·职场和发展
闲猫3 小时前
go orm GORM
开发语言·后端·golang
丁卯4043 小时前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
敲敲敲-敲代码5 小时前
【SQL实验】触发器
数据库·笔记·sql
Moonnnn.6 小时前
51单片机学习——动态数码管显示
笔记·嵌入式硬件·学习·51单片机
电棍2338 小时前
verilog笔记
笔记·fpga开发
让我安静会8 小时前
Obsidian·Copilot 插件配置(让AI根据Obsidian笔记内容进行对话)
人工智能·笔记·copilot
世事如云有卷舒9 小时前
FreeRTOS学习笔记
笔记·学习
羊小猪~~12 小时前
MYSQL学习笔记(九):MYSQL表的“增删改查”
数据库·笔记·后端·sql·学习·mysql·考研
yuanbenshidiaos12 小时前
【数据挖掘】数据仓库
数据仓库·笔记·数据挖掘