同步与互斥
互斥量
在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为互斥锁的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。Go 语言的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex
sync.Mutex
// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
//
// In the terminology of the Go memory model,
// the n'th call to Unlock “synchronizes before” the m'th call to Lock
// for any n < m.
// A successful call to TryLock is equivalent to a call to Lock.
// A failed call to TryLock does not establish any “synchronizes before”
// relation at all.
type Mutex struct {
state int32
sema uint32
}
加锁
func (m *Mutex) Lock()
func (m *Mutex) TryLock() bool
解锁
func (m *Mutex) Unlock()
sync.Mutex 提供了两种不同的模式,即正常模式和饥饿模式
- 正常模式(Normal Mode): 正常模式是
sync.Mutex的默认模式,也是大多数情况下所使用的模式。在正常模式下,协程在等待锁时,会进入一个队列并等待锁的释放。一旦锁被释放,等待的协程中的一个会被唤醒并获得锁。这种模式可以保证公平性,即所有协程都有机会获得锁,避免了某些协程长时间等待锁的问题。 - 饥饿模式(Starvation Mode): 饥饿模式是一种为了减少竞争和提高性能的模式。在饥饿模式下,锁的所有权会优先授予已经等待较长时间的协程,而不是先入先出原则。这可以避免某些协程因为频繁等待锁而引起的性能问题。饥饿模式的引入是为了优化某些特定场景下的性能,但可能会导致某些协程长时间等待锁而引起公平性问题
sync.RWMutex
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount atomic.Int32 // number of pending readers
readerWait atomic.Int32 // number of departing readers
}
加锁
func (rw *RWMutex) Lock()
func (rw *RWMutex) RLock()
func (rw *RWMutex) TryLock() bool
func (rw *RWMutex) TryRLock() bool
解锁
func (rw *RWMutex) RUnlock()
func (rw *RWMutex) Unlock()
条件变量
条件变量是基于互斥锁的一种同步工具。在 Go 语言中,我们需要用 sync.NewCond 函数来初始化一个 sync.Cond 类型的条件变量
初始化条件变量
var lock sync.Mutex
cond := sync.NewCond(&lock)
阻塞当前 goroutine
条件变量的 Wait 方法需要在它基于的互斥锁保护下执行,否则就会引发不可恢复的 panic
func (c *Cond) Wait()
唤醒 goroutine
func (c *Cond) Signal()
func (c *Cond) Broadcast()
为什么先要锁定条件变量基于的互斥锁,才能调用它的 Wait 方法?
条件变量的 Wait 方法在阻塞当前的 goroutine 之前,会解锁它基于的互斥锁,所以在调用该 Wait 方法之前,我们必须先锁定那个互斥锁,否则在调用这个 Wait 方法时,就会引发一个不可恢复的 panic
为什么要用 for 语句来包裹调用其 Wait 方法的表达式?
这主要是为了防止虚假唤醒而导致出错。如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的 Wait 方法,并继续等待下次通知的到来
原子操作
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换
Go 语言的 sync/atomic 包中的函数可以做的原子操作有:加法(Add)、比较并交换(Compare And Swap,CAS)、加载(Load)、存储(Store)和交换(Swap)。这些函数针对的数据类型并不多。但是,对这些类型中的每一个,sync/atomic 包都会有一套函数给予支持。这些数据类型有:int32、int64、uint32、uint64、uintptr,以及 unsafe 包中的 Pointer。不过,针对 unsafe.Pointer 类型,该包并未提供进行原子加法操作的函数
func AddInt32(addr *int32, delta int32) (new int32)
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)
func SwapInt32(addr *int32, new int32) (old int32)
atomic.Value
- 参数值不能为 nil
- 参数值的类型必须与首个被存储值的类型相同
sync.Once
sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等。作用与 init 函数类似,但有区别
- init 函数是当所在的 package 首次被加载时执行,若迟迟未被使用,则既浪费了内存,又延长了程序加载时间
- sync.Once 可以在代码的任意位置初始化和调用,因此可以延迟到使用时再执行,并发场景下是线程安全的
// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
//
// In the terminology of the Go memory model,
// the return from f “synchronizes before”
// the return from any call of once.Do(f).
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/386),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
func (o *Once) Do(f func())
sync.WaitGroup
WaitGroup 用于等待特定数量的 goroutines 执行完成
type WaitGroup struct {
noCopy noCopy
state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
sema uint32
}