不要使用共享数据来通信,使用通信来共享数据

无论任何时候,只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争

避免数据竞争的三种方式:

  1. 不去’写’变量
  2. 避免多个goroutine访问变量,变量都被限定在一个goroutine中, 其他的goroutine使用channel来发送请求给指定goroutine去更新变量,这就是使用通信来共享变量,
    • 即使当一个变量无法在其整个生命周期内被绑定到一个独立的goroutine,绑定依然是并发问题的一个解决方案。例如在一条流水线上的goroutine之间共享变量是很普遍的行为,在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能够避免在将变量传送到下一阶段后再去访问它,那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段,传送完之后被绑定到下一个,以此类推。这种规则有时被称为串行绑定。
  3. 第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”

sync的原子性操作

1
2
3
4
5
6
7
8
9
// NOTE: not atomic!
func Withdraw(amount int) bool {
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}

上面这个函数在过多的取款操作同时执行时,balance可能瞬间小于0,导致一些并发的取款被拒绝,这里的问题是取款不是一个原子操作,里面有三个步骤
为了解决这个问题,最直接的想法是在Withdraw函数里面加锁,但是go的锁是不可重入的,这么做就会导致Withdraw拿着锁,等着Deposit执行,Dsposit等着拿到锁执行,这就会导致死锁

一个通用的解决方法是,将函数拆分为加锁的公开方法和一个不加锁的私有方法

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
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false // insufficient funds
}
return true
}

func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}

func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}

// This function requires that the lock be held.
func deposit(amount int) { balance += amount }