本文主要讨论同步机制 的相关内容。
目录 Table of Contents
前情提要
尽管 Go
提倡“不要通过共享内存来通信,而应该通过通信来共享内存” ,亦即使用 channel
而非使用 sync
。然而支持同步机制仍然十分重要,无论是出于兼容或是方便 的目的。
在《现代操作系统(第4版)》第2.3节-进程间通信 中,提及了竞争条件 (race condition
) 和临界区域 (critical region
) 的概念,本节讨论重点落于通过互斥方案以解决经典 IPC
问题 。其中包含了多种思路、工具和机制,如屏蔽中断、锁变量、严格轮换法、Peterson
解法、TSL
指令、睡眠与唤醒、互斥量、信号量、管程、消息传递、屏障、避免锁等。Go
的 channel
很明显借鉴了消息传递的解决方法,而 sync
包则对应了互斥量、信号量、睡眠与唤醒/管程、屏蔽中断的具体实现。
本文将讨论 Go
并发编程中的各式各样的同步机制 ,包括悲观并发控制和乐观并发控制,如有错漏,欢迎指出 ;P
互斥量
互斥量 (Mutex
) 在 Golang
中对应了各种锁 (Lock
),包括互斥锁、读写锁、双重检查锁。互斥量主要用于通过锁机制实现悲观并发控制 ,锁住临界区 (critical section
) 以保证同时刻只有一个 goroutine
可以访问共享资源 (shared resource
) 。
互斥锁 sync.Mutex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 var mu sync.Mutexfunc DoSthToData () { mu.Lock() defer mu.Unlock() }() type SomeObj struct { mu *sync.Mutex } func NewSomeObj () *SomeObj { return &SomeObj{ mu: &sync.Mutex{} } } func (o *SomeObj) DoSthToObj () { o.mu.Lock() defer o.mu.Unlock() }
g1
已经加锁后,g2
再尝试加锁,g2
造成阻塞;g2
已经解锁后,g2
将加锁成功,g2
恢复运行。
无论是读还是写,均会互斥。
读写锁 sync.RWMutex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var mu sync.Mutexfunc WriteMutex () { mu.Lock() defer mu.Unlock() } func ReadMutex () { mu.RLock() defer mu.RUnlock() }
读写锁将数据或对象设定为写模式(只写)或者读模式(只读)。
写模式下,多个写操作之间是互斥的,即一路只写;读模式下,多个读操作之间不会互斥,即多路只读;写操作和读操作之间是互斥的。
所有被读锁定 (RLock
) 的 goroutine
会在写解锁 (Unlock
) 时唤醒;读解锁 (RUnlock
) 只会在没有任何读锁定时唤醒一个要进行写锁定 (Lock
) 而被阻塞的 goroutine
。
双重检查锁 sync.Once
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type Singleton struct {}var instance *Singletonvar mu sync.Mutexfunc GetSingleton () *Singleton { if instance == nil { mu.Lock() defer mu.Unlock() if instance == nil { instance = &Singleton{} } } return instance }
第一次检查:控制共享资源访问。
第二次检查:解决锁竞争带来的重复创建单例问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 type Singleton struct {}var instance *Singletonfunc init () { instance = &Singleton{} } func GetSingleton () *Singleton { return instance }
利用 init
函数只会被执行一次的特点,在程序运行前创建单例。
由于 init
函数没有参数值和返回值,有一定的局限性。
1 2 3 4 5 6 7 8 9 10 11 12 13 type Singleton struct {}var instance *Singletonvar once sync.Oncefunc GetSingleton () *Singleton { once.Do(func () { instance = &Singleton{} }) return instance }
如果存在某个只执行一次的任务,不适合放在 init
函数,可考虑放在 sync.Once
类中。
sync.Once
内部使用了卫述语句、双重检查锁、原子操作等实现全局唯一操作。
信号量
信号量 (Semaphore
) 在 Golang
中对应的是等待组 (sync.WaitGroup
)。信号量主要用于通过计数 来协调多个 goroutine
有序运行,等待批量并发任务运行结束。信道也可以实现类似功能,相比之下通过等待组的方式更为轻量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { var wg sync.WaitGroup var mu sync.Mutex for i:=0 ; i++; i<5 { wg.Add(1 ) go func () { defer wg.Done() mu.Lock() defer mu.Unlock() }() } wg.Wait() }
等待组内部拥有计数器,通常而言:计数器增加 (Add
) 、计数器减少 (Done
) 和计数器等待 (Wait
) 这三个方法应该整体出现。
等待组与同步锁配合使用,等待组解决批量任务的协调问题,同步锁解决单个任务的竞争问题。
条件变量
条件变量 (Condition Variables
) 在 Golang
中对应的是条件变量 (sync.Cond
)。条件变量主要用于通过条件 来协调多个 goroutine
有序运行,唤醒单个或多个正在睡眠的协程。信道也可以实现类似功能,相比之下通过条件变量的方式更为轻量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func main () { cond := sync.Cond(&sync.Mutex{}) condition := false go func () { cond.L.Lock() defer cond.L.Unlock() condition = true cond.Signal() }() go func () { cond.L.Lock() defer cond.L.Unlock() condition = true cond.Broadcast() }() cond.L.Lock() defer cond.L.Unlock() for !condition { cond.Wait() } }
条件变量内部拥有同步锁,通常而言:等待 (Wait
) 与单发 (Signal
) 或广播 (Broadcast)
这两个方法应该配对出现。
条件变量与同步锁配合使用,条件变量解决协程处于睡眠或唤醒中的状态问题,同步锁解决单个任务的竞争问题。
原子操作
原子操作 (Atomic Operation
) 在 Golang
中对应的是原子操作 (sync/atomic
)。原子操作实现互斥的核心是:执行过程中 CPU
不能被中断,并基于这个基础上实现乐观并发控制 。
增加与减少 Add
1 2 newi32 := atomic.AddInt32(&i32, 1 ) newi32 := atomic.AddInt32(&i32, -1 )
有符号整数型:增加使用正数,减少使用负数。
无符号整数型:增加使用原码,减少使用补码。
指针类型:在 Golang
中不支持直接对指针进行运算。
比较与替换 CAS
1 2 isSwap := atomic.CompareAndSwapInt32(&addr, oldVal, newVal) oldVal := atomic.SwapInt32(&addr, newVal)
CAS
的优势:无锁机制,减少锁竞争的性能损耗。
CAS
的劣势:被操作值频繁变更,比较与替换则容易失败,需要引入循环进行多次尝试。
CAS
不会阻塞协程,但会暂时停滞。对于一些类型的值,乐观并发控制 (CAS
) 优于悲观并发控制 (Lock
)。
载入与存储 Load/Store
1 2 v := atomic.LoadInt32(&value) atomic.StoreInt32(&value, v)
Swap
比 CompareAndSwap
约束少(不做比较),比 Load
功能强(返回旧值)。
CompareAndSwap
可能会失败,Load
总是能成功。
参考链接