《GO语言圣经》学习笔记(九)基于信号量的并发
知识点
竞争条件
竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。竞争条件是很恶劣的一种场景,因为这种问题会一直潜伏在你的程序里,然后在非常少见的时候蹦出来,或许只是会在很大的负载时才会发生,又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断。
避免竞争条件
- 第一种方法是不要去写变量
- 第二种避免数据竞争的方法是,避免从多个goroutine访问变量
- 第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问
串行绑定
如果流水线的每一个阶段都能够避免在将变量传送到下一阶段后再去访问它,那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段,传送完之后被绑定到下一个,以此类推。这种规则有时被称为串行绑定。
sync.Mutex互斥锁
使用buffered channel作为一个计数信号量,可以简单的实现一个锁,一个只能为1和0的信号量叫做二元信号量(binary semaphore):
var (
sema = make(chan struct{}, 1) // a binary semaphore guarding balance
balance int
)
func Deposit(amount int) {
sema <- struct{}{} // acquire token
balance = balance + amount
<-sema // release token
}
func Balance() int {
sema <- struct{}{} // acquire token
b := balance
<-sema // release token
return b
}
sync.Mutex使这种实现变得更加简单和易用,善用defer
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
go里没有重入锁,理由很简单,当前goroutinue重入对于同一个变量来说也是一种不确定性的引入,be simple!
sync.RWMutex读写锁
允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex:
var mu sync.RWMutex
var balance int
func Balance() int {
mu.RLock() // readers lock
defer mu.RUnlock()
return balance
}
内存同步
在现代计算机中,一般都会有多个 CPU,每个有 CPU 有自己的主存缓存。为了性能,写到主存的数据一般都会在每个 CPU 内部首先缓存起来,然后在必要的时候提交到主存。这些修改的提交的顺序可能和 goroutine 的执行顺序不同
。而同步原语比如说 channel 或者互斥锁的主要目的就是让 CPU 把 buffer 的数据提交到主存中
,以便其它执行在其它 CPU 上的 goroutine 能够看到这些提交带来变化。
所有并发的问题都能被这些已经建立的简单模式所解决:
要么保证变量只在单个 goroutine 中使用;
要么在多个 goroutine 之间使用互斥锁。
sync.Once初始化
sync.Once需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了,示例如下,每一次对Do(loadIcons)的调用都会锁定mutex,并会检查boolean变量。在第一次调用时,boolean变量的值是false,Do会调用loadIcons并会将boolean变量设置为true。随后的调用什么都不会做,但是mutex同步会保证loadIcons对内存产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话,我们能够避免在变量被构建完成之前
和其它goroutine共享该变量。
var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
引用
欢迎大家关注我的公众号
半亩房顶