在实际项目中,线程安全的问题肯定会涉及到,这篇文章就总结Golang中有哪些常见的数据结构是线程安全的,以及他们的使用场景。
常见数据结构:
sync.Mutex
:这是一种互斥锁,可以用来保护对共享数据的访问。使用时,需要在访问共享数据的代码块之前调用Lock
方法,在代码块执行完毕后调用Unlock
方法。这是Golang中最基本的悲观锁,很多的数据结构都是通过sync.Mutex
来实现线程安全。chan
:这是 Go 中的通道,可以在多个goroutine
之间进行数据传递。在通道的发送和接收操作中,Go 会自动进行加锁,保证线程安全,可以从goroutine
的源码中看出,其结构中有lock
的字段。
type hchan struct {
qcount uint // 循环数组中的数量
dataqsiz uint // 循环数组中的size
// channel分为有缓冲和无缓冲两种
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16 // channel中的元素大小
closed uint32 // channel是否关闭
elemtype *_type // channel中的元素类型
sendx uint // 循环数组中的下一次发送下标位置
recvx uint // 循环数组中的下一次接受下标位置
// 尝试读取channel或向channel写入数据而被阻塞的goroutine
// waitq 是一个双向链表
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
sync.RWMutex
:这是一种读写锁,可以用来保护对共享数据的访问。与互斥锁不同的是,读写锁允许多个goroutine
同时读取共享数据,但在写入时会阻塞读取操作。
type RWMutex struct {
w Mutex // 互斥锁用于保证写操作的独占访问
writerSem uint32 // 保护写操作的独占访问,同时也用于记录当前有多少个写操作正在进行。
readerSem uint32 // 保护读操作的独占访问,同时也用于记录当前有多少个读操作正在进行。
readerCount int32 // 计数器,用于记录当前有多少个读操作正在进行
readerWait int32 // 计数器,用于记录写操作阻塞时的读操作数量
}
在获取读锁时,RWMutex
会先使用互斥锁保护计数器,并将计数器加 1。如果当前没有写操作正在进行,则会立即返回;否则,RWMutex
会使用互斥锁将当前 goroutine
阻塞,直到所有写操作完成为止。在释放读锁时RWMutex
会再次使用互斥锁保护计数器,并将计数器减 1。如果计数器减为 0,则表明当前没有读操作正在进行,RWMutex
会唤醒所有被阻塞的写操作。在获取写锁时,RWMutex
会使用互斥锁阻塞所有的读操作和写操作,直到当前写操作完成为止。
sync.Once
:这是一种用于保证某段代码仅执行一次的工具。使用时,可以通过调用Do
方法来执行指定的代码块,如果之前已经调用过Do
方法,那么这次调用就会被忽略。
import "sync"
type singleton struct {
}
var (
instance *singleton
once sync.Once
)
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{
}
})
return instance
}
sync.Map
:这是一种并发安全的map
,可以在多个goroutine
之间安全地读写。底层层实现逻辑和chan
一样,也是通过sync.Mutex
来实现的。sync.WaitGroup
:这是一种用于等待一组goroutine
执行完毕的工具。使用时,可以通过调用Add
方法来指定等待的goroutine
数量,然后在每个goroutine
执行完毕后调用Done
方法来通知WaitGroup
。最后,可以调用Wait
方法来阻塞当前goroutine
,直到所有等待的goroutine
执行完毕。
其他的数据结构:
sync.Pool
:这是一种对象池,可以用来缓存对象,避免频繁地分配和释放内存。使用时,可以通过调用Put
方法将对象放入池中,通过调用Get
方法获取对象。sync.Cond
:这是一种条件变量,可以用来在多个goroutine
之间同步执行。使用时,可以通过调用Wait
方法阻塞当前goroutine
,直到满足特定条件时被唤醒,或者通过调用Signal
方法唤醒一个被阻塞的goroutine
。sync.Locker
:这是一个接口,定义了加锁、解锁、尝试加锁的操作。实现了这个接口的类型都是线程安全的。
虽然这些数据结构是线程安全的,但使用它们时仍需要注意一些问题,例如:
- 避免死锁:使用互斥锁时,要注意避免两个 goroutine 之间相互等待对方释放锁,从而导致死锁。
- 尽量减少加锁时间:加锁会影响性能,应尽量减少加锁时间。
- 使用适当的锁类型:应根据需要选择适当的锁类型,例如读多写少时可以使用读写锁,避免写操作阻塞读操作。
在 Go 中,还有一些数据结构是默认是非线程安全的,例如:
map
:这是Go
中的内置map
类型,默认是非线程安全的。如果需要在多个goroutine
之间安全地读写map
,可以使用sync.Map
或自己实现加锁机制。slice
:这是Go
中的内置slice
类型,默认是非线程安全的。如果需要在多个goroutine
之间安全地读写slice
,可以使用互斥锁或读写锁进行保护。- 结构体:
Go
中的结构体默认是非线程安全的。如果需要在多个goroutine
之间安全地读写结构体,可以使用互斥锁或读写锁进行保护。