Go 互斥与同步

Dec 11, 2019 23:00 · 658 words · 2 minute read Golang

原子操作

原子操作是 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 还没有开始就先调用,否则可能出现某个任务还没有开始执行就被认为结束了。