1. sync.Mutex互斥锁

不同goroutine之间对公共资源进行访问需要使用互斥锁。例如在对银行账户的操作中,如果我们有两种操作,一个是查询余额,一个是存款。其操作如下:

package bank 

// 存款余额
var balance int 

// 存款
func Deposit(amount int) { balance = balance + amount } 

// 查询
func Balance() int { return balance }
// Alice: 
go func() { 
    bank.Deposit(200)  // A1
    fmt.Println("=", bank.Balance()) // A2
}()

// Bob: 
go bank.Deposit(100) // B

这其中,若把A1分为两个操作,A1r:把余额从内存中读出来;A2w:把修改后的余额写入内存。
若执行顺序为A1r → B → A1w → A2, 正常情况下,Alice和Bob分别存入了$200,$100,因此最后的存款应该是300,但最后输出结果为200。因为A在计算时是按照A1r读出的数值进行计算,忽略了B的操作,A与B之间发生了数据竞争。

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

解决此问题的办法之一是使用互斥锁。

import "sync"
var ( 
    mu  sync.Mutex // guards balance
    balance int
)

func Deposit(amount int) { 
    mu.Lock() 
    defer mu.Unlock()
    balance = balance + amount 
}

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

每次一个goroutine访问bank变量时(这里只有balance余额变量),它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话,这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态.

2. sync.RWMutex读写锁

由于Balance函数只需要读取变量的状态,所以我们同时让多个Balance调用并发运行事实上 是安全的,只要在运行的时候没有存款或者取款操作就行。在这种场景下我们需要一种特殊 类型的锁,其允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁 (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
}

读写锁的规则是

  • 读锁不能阻塞读锁
  • 读锁需要阻塞写锁,直到所有读锁都释放
  • 写锁需要阻塞读锁,直到所有写锁都释放
  • 写锁需要阻塞写锁