概述
我一直在找一种好的方法来解释 go 语言的并发模型:
不要通过共享内存来通信,相反,应该通过通信来共享内存
但是没有发现一个好的解释来满足我下面的需求:
1.通过一个例子来说明最初的问题
2.提供一个共享内存的解决方案
3.提供一个通过通信的解决方案
这篇文章我就从这三个方面来做出解释。
读过这篇文章后你应该会了解通过通信来共享内存的模型,以及它和通过共享内存来通信的区别,你还将看到如何分别通过这两种模型来解决访问和修改共享资源的问题。
前提
设想一下我们要访问一个银行账号:
type Account interface {
Withdraw(uint)
Deposit(uint)
Balance() int
}
type Bank struct {
account Account
}
func NewBank(account Account) *Bank {
return &Bank{account: account}
}
func (bank *Bank) Withdraw(amount uint, actor_name string) {
fmt.Println("[-]", amount, actor_name)
bank.account.Withdraw(amount)
}
func (bank *Bank) Deposit(amount uint, actor_name string) {
fmt.Println("[+]", amount, actor_name)
bank.account.Deposit(amount)
}
func (bank *Bank) Balance() int {
return bank.account.Balance()
}
</div>
因为 Account 是一个接口,所以我们提供一个简单的实现:
type SimpleAccount struct{
balance int
}
func NewSimpleAccount(balance int) *SimpleAccount {
return &SimpleAccount{balance: balance}
}
func (acc *SimpleAccount) Deposit(amount uint) {
acc.setBalance(acc.balance + int(amount))
}
func (acc *SimpleAccount) Withdraw(amount uint) {
if acc.balance >= int(mount) {
acc.setBalance(acc.balance - int(amount))
} else {
panic("杰克穷死")
}
}
func (acc *SimpleAccount) Balance() int {
return acc.balance
}
func (acc *SimpleAccount) setBalance(balance int) {
acc.add_some_latency() //增加一个延时函数,方便演示
acc.balance = balance
}
func (acc *SimpleAccount) add_some_latency() {
<-time.After(time.Duration(rand.Intn(100)) * time.Millisecond)
}
</div>
你可能注意到了 balance 没有被直接修改,而是被放到了 setBalance 方法里进行修改。这样设计是为了更好的描述问题。稍后我会做出解释。
把上面所有部分弄好以后我们就可以像下面这样使用它啦:
func main() {
balance := 80
b := NewBank(bank.NewSimpleAccount(balance))
fmt.Println("初始化余额", b.Balance())
b.Withdraw(30, "马伊琍")
fmt.Println("-----------------")
fmt.Println("剩余余额", b.Balance())
}
</div>
运行上面的代码会输出:
初始化余额 80
[-] 30 马伊琍
-----------------
剩余余额 50
</div>
没错!
不错在现实生活中,一个银行账号可以有很多个附属卡,不同的附属卡都可以对同一个账号进行存取钱,所以我们来修改一下代码:
func main() {
balance := 80
b := NewBank(bank.NewSimpleAccount(balance))
fmt.Println("初始化余额", b.Balance())
done := make(chan bool)
go func() { b.Withdraw(30, "马伊琍"); done <- true }()
go func() { b.Withdraw(10, "姚笛"); done <- true }()
//等待 goroutine 执行完成
<-done
<-done
fmt.Println("-----------------")
fmt.Println("剩余余额", b.Balance())
}
</div>
这儿两个附属卡并发的从账号里取钱,来看看输出结果:
初始化余额 80
[-] 30 马伊琍
[-] 10 姚笛
-----------------
剩余余额 70
</div>
这下把文章高兴坏了:)
结果当然是错误的,剩余余额应该是40而不是70,那么让我们看看到底哪儿出问题了。
问题
当并发访问共享资源时,无效状态有很大可能会发生。
在我们的例子中,当两个附属卡同一时刻从同一个账号取钱后,我们最后得到银行账号(即共享资源)错误的剩余余额(即无效状态)。
我们来看一下执行时候的情况:
处理情况
--------------
_马伊琍_|_姚笛_
1. 获取余额 80 | 80
2. 取钱 -30 | -10
3. 当前剩余 50 | 70
... | ...
4. 设置余额 50 ? 70 //该先设置哪个好呢?
5. 后设置的生效了
--------------
6. 剩余余额 70
</div>
上面 ... 的地方描述了我们 add_some_latency 实现的延时状况,现实世界经常发生延迟情况。所以最后的剩余余额就由最后设置余额的那个附属卡决定。
解决办法
我们通过两种方法来解决这个问题:
1.共享内存的解决方案
2.通过通信的解决方案
所有的解决方案都是简单的封装了一下 SimpleAccount 来实现保护机制。
共享内存的解决方案
又叫 “通过共享内存来通信”。
这种方案暗示了使用锁机制来预防同时访问和修改共享资源。锁告诉其它处理程序这个资源已经被一个处理程序占用了,因此别的处理程序需要排队直到当前处理程序处理完毕。
让我们来看看 LockingAccount 是怎么实现的:
type LockingAccount struct {
lock sync.Mutex
account *SimpleAccount
}
//封装一下 Simp

