当我们使用线性编程方式进行开发时,程序逻辑的执行步骤会按我们所预测的一样执行。但当使用并发方式开发时,程序执行的顺序变得不可预测。
线性和并发方式对比,为了能充分的利用到CPU,会频繁的进行上下文切换。

在并发编程中,首当其冲的问题是如何保证数据的准确性?
数据竞争
这里引用《Go 语言圣经》银行存款的示例:
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
一般情况下,最终balance变量会是300,但也不是一定的。由于Deposit方法不是原子的,Bob的存款计算会被Alice的余额覆盖,导致数据错误。
sync.Mutex 互斥锁
为了避免直接的数据竞争,我们可以在数据变化的前面加把锁,更新完成释放锁,让其变成原子操作。
手动实现互斥锁
这里定义一个sema信号量channel,大小为1。当调用Deposit时首先往channel里发送一个空struct。如果发送不成功即阻塞,这一步是获取锁。完成逻辑释放锁。
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
}
使用Go提供的互斥锁
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()
b := balance
return b
}
sync.RWMutex 读写锁
上面的互斥锁解决了由数据竞争导致出错的问题,但是每次读取余额也需要获取锁,这增加了和其他goroutine的竞争。
一般我们并发调用Balance是安全的,只要这期间没有数据变更。
读写锁基于互斥锁实现,允许并发读。
import "sync"
var (
mu sync.RWMutex
balance int
)
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
balance = balance + amount
}
func Balance() int {
mu.RLocker()
defer mu.RUnlock()
b := balance
return b
}