认识 Golang 锁

当我们使用线性编程方式进行开发时,程序逻辑的执行步骤会按我们所预测的一样执行。但当使用并发方式开发时,程序执行的顺序变得不可预测。

线性和并发方式对比,为了能充分的利用到CPU,会频繁的进行上下文切换。

线性和并发对比

在并发编程中,首当其冲的问题是如何保证数据的准确性?

数据竞争

这里引用《Go 语言圣经》银行存款的示例:

1
2
3
4
5
6
7
8
var balance int

func Deposit(amount int) {
	balance = balance + amount
}
func Balance() int {
	return balance
}

并发存款

1
2
3
4
5
6
7
8
// 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。如果发送不成功即阻塞,这一步是获取锁。完成逻辑释放锁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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提供的互斥锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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是安全的,只要这期间没有数据变更。

读写锁基于互斥锁实现,允许并发读。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
}

参考

9.2. sync.Mutex互斥锁

updatedupdated2021-12-012021-12-01
加载评论