075-互斥锁
Posted --Allen--
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了075-互斥锁相关的知识,希望对你有一定的参考价值。
过去我们在写 C/C++ 程序的时候,总会听到竞争的概念。比如多线程对共享变量的存取,需要加锁才能避免数据不致的问题。
在 Golang 中也不例外,多个 Goroutine 存取共享变量,同样也会出现竞争问题。
1. 银行转账
假设 Alice 有一个银行账户。Alice 会往里存钱,她的好友 Bob 也会转钱给她。用程序来描述应该是这样:
package bank
// Alice 的账户余额
var balance int
// 存钱
func Deposit(amount int)
balance = balance + amount
// 查看余额
func Balance() int
return balance
有了以上的模拟存钱的函数后,Alice 和 Bob 就可以存钱啦。比如:
// 存入 200
bank.Deposit(200)
- 单 goroutine 顺序存取
如果 Alice 存入 200,Bob 存入 100,那看起来就像这样:
func alice(amount int)
fmt.Printf("Alice deposit $%d\\n", amount)
bank.Deposit(amount)
func bob(amount int)
fmt.Printf("Bob deposit $%d\\n", amount)
bank.Deposit(amount)
func main()
alice(200);
bob(100);
fmt.Printf("balance: $%d\\n", bank.Balance()) // balance: 300
- 并发存取
看起来我们的程序似乎也没什么问题。但是考虑一下,Alice 和 Bob 是两个人,他们有可能会同时往账户里存钱,也就是说,Alice 和 Bob 存钱的时候 ,调用 Deposit 函数是并发的。
func main()
var wg sync.WaitGroup
wg.Add(2)
go func()
alice(100)
wg.Done()
()
go func()
bob(200)
wg.Done()
()
wg.Wait()
fmt.Printf("balance: $%d\\n", bank.Balance()) // balance: 300
这个时候,余额还能是 300 吗?
2. 竞争分析
我们重点看存钱的操作,这是并发的。
// 存钱
func Deposit(amount int)
balance = balance + amount
假设 Alice 在存钱的时候,语句 balance = balance + amount 执行到一半,即 balance + amount 的结果计算出来,是 200,并赋值给 balance 的时候,Goroutine 被切换出去。
这时候执行 Bob 的 Goroutine,Bob 存入了 100,完成后,Alice 的 Goroutine 又切换回来,继续执行之前的赋值操作,将 200 赋值给 balance。
最后的结果是 200,而不是 300.
有一定基础的同学分析起这个竞态条件非常简单,因为 balance 是全局共享的。Deposit 函数引用了全局共享的变量,因此 Deposit 也是协程不安全函数。
3. 解决方案
3.1 使用 channel
为了解决这种协程不安全问题,对 balance 一定要互斥访问。前面我们学过了 channel,这很容易做到。
package bank
// Alice 的账户余额
var balance int
// channel 信号量
var sema = make(chan struct, 1)
// 存钱
func Deposit(amount int)
sema <- struct
balance = balance + amount
<-sema
// 查看余额
// 你可能会好奇,为什么这里对 balance 的读取也会加锁,直接 return balance 不就好了吗?
// 这是一个更加深入的话题,我们后面会继续讨论。
func Balance() int
sema <- struct
b := balance
<-sema
return b
3.2 使用互斥锁
实际上在 Golang 里提供了更加方便的结构来帮助我们做这件事,sync.Mutex. 这和你在其它地方看到的 Mutex 是一个概念。我们使用 Mutex 来重写上面的程序。
package bank
import "sync"
// Alice 的账户余额
var balance int
// channel 信号量
var mu sync.Mutex
// 存钱
func Deposit(amount int)
mu.Lock()
defer mu.Unlock()
balance = balance + amount
// 查看余额
func Balance() int
mu.Lock()
defer mu.Unlock()
return balance
由于 sync.Mutex 提供了 Lock 和 Unlock 方法,我们可以使用 defer 来搭配 Lock 和 Unlock,这样看起来会更加简洁。
4. 总结
实际上,我们并不推荐在 Golang 里使用互斥量。Golang 的口头禅是:
Do not communicate by sharing memory; instead, share memory by communicating.
“不要使用共享内存进行通信,应该使用通信来共享数据”。下一节,我们来演示如何使用通信的方式来共享数据。
以上是关于075-互斥锁的主要内容,如果未能解决你的问题,请参考以下文章