Go 互斥与同步
Dec 11, 2019 23:00 · 658 words · 2 minute read
原子操作
原子操作是 CPU 的能力,与操作系统无关。
原子操作不会被打断,与执行体的种类无关,无论 goroutine 还是操作系统线程都适用。
var val int32
// ...
newval := atomic.AddInt32(&val, delta)
等价于
var val int32
var mutex sync.Mutex
// ...
mutex.Lock()a
val += delta
newval = val
mutex.Unlock()
原子操作可以用互斥体来实现,但是原子操作快得多。
互斥体(锁)
在操作需要互斥的数据前,先调用 Lock()
,操作完成后就调用 Unlock()
。锁的确会导致代码串行执行,所以在某段代码并发度非常高的情况下,串行执行的确会导致性能的显著降低。但相比其他的进程内通讯的原语来说,锁并不慢。从进程内通讯来说,比锁快的东西,只有原子操作。
锁最大的问题在于不易控制,忘记解锁后果是灾难性的,相当于服务器挂了。
mutex.Lock()
defer mutex.Unlock()
doSth()
defer
语句可以保证即使 doSth()
发生异常仍会解锁。
锁不容易控制的另一个表现是锁粒度的问题。doSth()
函数里面如果出现好几秒才返回的慢网络 IO 请求,那么这几秒对服务器来说就好像挂了,无法处理请求。
牢记:不要在锁里面执行费时操作。
在锁的最佳编程实践中,如果一组数据的并发访问符合“读多写少”的特征,应该用读写锁。
读操作:
mutex.RLock()
defer mutex.RUnlock()
doReadOnlyThings
写操作:
mutex.Lock()
defer mutex.Unlock()
doWriteThings
可以并发读,但是独占写。
牢记:读操作不阻止读操作,阻止写操作;写操作阻止一切,不管读操作还是写操作。
等待组
同步的一个最常见的场景是把一个大任务分解为 n 个小任务,分配给 n 个执行体并行去做,等待它们一起做完。
var wg sync.WaitGroup
// ...
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
// doTask
}()
}
wg.Wait()
- 在每个任务开始的时候调用
wg.Add(1)
- 在每个任务结束的时候调用
wg.Done()
- 在主协程调用
wg.Wait()
等任务结束
wg.Add(1)
是要在任务的 goroutine 还没有开始就先调用,否则可能出现某个任务还没有开始执行就被认为结束了。