概述
我一直在找一種好的方法來(lái)解釋 go 語(yǔ)言的并發(fā)模型:
不要通過(guò)共享內(nèi)存來(lái)通信,相反,應(yīng)該通過(guò)通信來(lái)共享內(nèi)存
但是沒(méi)有發(fā)現(xiàn)一個(gè)好的解釋來(lái)滿足我下面的需求:
1.通過(guò)一個(gè)例子來(lái)說(shuō)明最初的問(wèn)題
2.提供一個(gè)共享內(nèi)存的解決方案
3.提供一個(gè)通過(guò)通信的解決方案
這篇文章我就從這三個(gè)方面來(lái)做出解釋。
讀過(guò)這篇文章后你應(yīng)該會(huì)了解通過(guò)通信來(lái)共享內(nèi)存的模型,以及它和通過(guò)共享內(nèi)存來(lái)通信的區(qū)別,你還將看到如何分別通過(guò)這兩種模型來(lái)解決訪問(wèn)和修改共享資源的問(wèn)題。
前提
設(shè)想一下我們要訪問(wèn)一個(gè)銀行賬號(hào):
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()
}
因?yàn)?Account 是一個(gè)接口,所以我們提供一個(gè)簡(jiǎn)單的實(shí)現(xiàn):
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() //增加一個(gè)延時(shí)函數(shù),方便演示
acc.balance = balance
}
func (acc *SimpleAccount) add_some_latency() {
-time.After(time.Duration(rand.Intn(100)) * time.Millisecond)
}
你可能注意到了 balance 沒(méi)有被直接修改,而是被放到了 setBalance 方法里進(jìn)行修改。這樣設(shè)計(jì)是為了更好的描述問(wèn)題。稍后我會(huì)做出解釋。
把上面所有部分弄好以后我們就可以像下面這樣使用它啦:
運(yùn)行上面的代碼會(huì)輸出:
沒(méi)錯(cuò)!
不錯(cuò)在現(xiàn)實(shí)生活中,一個(gè)銀行賬號(hào)可以有很多個(gè)附屬卡,不同的附屬卡都可以對(duì)同一個(gè)賬號(hào)進(jìn)行存取錢,所以我們來(lái)修改一下代碼:
這兒兩個(gè)附屬卡并發(fā)的從賬號(hào)里取錢,來(lái)看看輸出結(jié)果:
這下把文章高興壞了:)
結(jié)果當(dāng)然是錯(cuò)誤的,剩余余額應(yīng)該是40而不是70,那么讓我們看看到底哪兒出問(wèn)題了。
問(wèn)題
當(dāng)并發(fā)訪問(wèn)共享資源時(shí),無(wú)效狀態(tài)有很大可能會(huì)發(fā)生。
在我們的例子中,當(dāng)兩個(gè)附屬卡同一時(shí)刻從同一個(gè)賬號(hào)取錢后,我們最后得到銀行賬號(hào)(即共享資源)錯(cuò)誤的剩余余額(即無(wú)效狀態(tài))。
我們來(lái)看一下執(zhí)行時(shí)候的情況:
上面 ... 的地方描述了我們 add_some_latency 實(shí)現(xiàn)的延時(shí)狀況,現(xiàn)實(shí)世界經(jīng)常發(fā)生延遲情況。所以最后的剩余余額就由最后設(shè)置余額的那個(gè)附屬卡決定。
解決辦法
我們通過(guò)兩種方法來(lái)解決這個(gè)問(wèn)題:
1.共享內(nèi)存的解決方案
2.通過(guò)通信的解決方案
所有的解決方案都是簡(jiǎn)單的封裝了一下 SimpleAccount 來(lái)實(shí)現(xiàn)保護(hù)機(jī)制。
共享內(nèi)存的解決方案
又叫 “通過(guò)共享內(nèi)存來(lái)通信”。
這種方案暗示了使用鎖機(jī)制來(lái)預(yù)防同時(shí)訪問(wèn)和修改共享資源。鎖告訴其它處理程序這個(gè)資源已經(jīng)被一個(gè)處理程序占用了,因此別的處理程序需要排隊(duì)直到當(dāng)前處理程序處理完畢。
讓我們來(lái)看看 LockingAccount 是怎么實(shí)現(xiàn)的:
//封裝一下 SimpleAccount
func NewLockingAccount(balance int) *LockingAccount {
return LockingAccount{account: NewSimpleAccount(balance)}
}
func (acc *LockingAccount) Deposit(amount uint) {
acc.lock.Lock()
defer acc.lock.Unlock()
acc.account.Deposit(amount)
}
func (acc *LockingAccount) Withdraw(amount uint) {
acc.lock.Lock()
defer acc.lock.Unlock()
acc.account.Withdraw(amount)
}
func (acc *LockingAccount) Balance() int {
acc.lock.Lock()
defer acc.lock.Unlock()
return acc.account.Balance()
}
直接明了!注意 lock sync.Lock,lock.Lock(),lock.Unlock()。
這樣每次一個(gè)附屬卡訪問(wèn)銀行賬號(hào)(即共享資源),這個(gè)附屬卡會(huì)自動(dòng)獲得鎖直到最后操作完畢。
我們的 LockingAccount 像下面這樣使用:
輸出的結(jié)果是:
現(xiàn)在結(jié)果正確了!
在這個(gè)例子中第一個(gè)處理程序加鎖后獨(dú)享共享資源,其它處理程序只能等待它執(zhí)行完成。
我們接著看一下執(zhí)行時(shí)的情況,假設(shè)馬伊琍先拿到了鎖:
現(xiàn)在我們的處理程序在訪問(wèn)共享資源時(shí)相繼的產(chǎn)生了正確的結(jié)果。
通過(guò)通信的解決方案
又叫 “通過(guò)通信來(lái)共享內(nèi)存”。
現(xiàn)在賬號(hào)被命名為 ConcurrentAccount,像下面這樣來(lái)實(shí)現(xiàn):
func NewConcurrentAccount(amount int) *ConcurrentAccount{
acc := ConcurrentAccount{
account : SimpleAccount{balance: amount},
deposits: make(chan uint),
withdrawals: make(chan uint),
balances: make(chan chan int),
}
acc.listen()
return acc
}
func (acc *ConcurrentAccount) Balance() int {
ch := make(chan int)
acc.balances - ch
return -ch
}
func (acc *ConcurrentAccount) Deposit(amount uint) {
acc.deposits - amount
}
func (acc *ConcurrentAccount) Withdraw(amount uint) {
acc.withdrawals - amount
}
func (acc *ConcurrentAccount) listen() {
go func() {
for {
select {
case amnt := -acc.deposits:
acc.account.Deposit(amnt)
case amnt := -acc.withdrawals:
acc.account.Withdraw(amnt)
case ch := -acc.balances:
ch - acc.account.Balance()
}
}
}()
}
ConcurrentAccount 同樣封裝了 SimpleAccount ,然后增加了通信通道
調(diào)用代碼和加鎖版本的一樣,這里就不寫了,唯一不一樣的就是初始化銀行賬號(hào)的時(shí)候:
運(yùn)行產(chǎn)生的結(jié)果和加鎖版本一樣:
讓我們來(lái)深入了解一下細(xì)節(jié)。
通過(guò)通信來(lái)共享內(nèi)存是如何工作的
一些基本注意點(diǎn):
共享資源被封裝在一個(gè)控制流程中。
結(jié)果就是資源成為了非共享狀態(tài)。沒(méi)有處理程序能夠直接訪問(wèn)或者修改資源。你可以看到訪問(wèn)和修改資源的方法實(shí)際上并沒(méi)有執(zhí)行任何改變。
func (acc *ConcurrentAccount) Withdraw(amount uint) {
acc.withdrawals - amount
}
訪問(wèn)和修改是通過(guò)消息和控制流程通信。
在控制流程中任何訪問(wèn)和修改的動(dòng)作都是相繼發(fā)生的。
當(dāng)控制流程接收到訪問(wèn)或者修改的請(qǐng)求后會(huì)立即執(zhí)行相關(guān)動(dòng)作。讓我們仔細(xì)看看這個(gè)流程:
select 不斷地從各個(gè)通道中取出消息,每個(gè)通道都跟它們所要執(zhí)行的操作相一致。
重要的一點(diǎn)是:在 select 聲明內(nèi)部的一切都是相繼執(zhí)行的(在同一個(gè)處理程序中排隊(duì)執(zhí)行)。一次只有一個(gè)事件(在通道中接受或者發(fā)送)發(fā)生,這樣就保證了同步訪問(wèn)共享資源。
領(lǐng)會(huì)這個(gè)有一點(diǎn)繞。
讓我們用例子來(lái)看看 Balance() 的執(zhí)行情況:
1. b.Balance() |
2. ch -> [acc.balances]-> ch
3. -ch | balance = acc.account.Balance()
4. return balance -[ch]- balance
5 |
這兩個(gè)流程都干了點(diǎn)什么呢?
附屬卡的流程
1.調(diào)用 b.Balance()
2.新建通道 ch,將 ch 通道塞入通道 acc.balances 中與控制流程通信,這樣控制流程也可以通過(guò) ch 來(lái)返回余額
3.等待 -ch 來(lái)取得要接受的余額
4.接受余額
5.繼續(xù)
控制流程
1.空閑或者處理
2.通過(guò) acc.balances 通道里面的 ch 通道來(lái)接受余額請(qǐng)求
3.取得真正的余額值
4.將余額值發(fā)送到 ch 通道
5.準(zhǔn)備處理下一個(gè)請(qǐng)求
控制流程每次只處理一個(gè) 事件。這也就是為什么除了描述出來(lái)的這些以外,第2-4步?jīng)]有別的操作執(zhí)行。
總結(jié)
這篇博客描述了問(wèn)題以及問(wèn)題的解決辦法,但那時(shí)沒(méi)有深入去探究不同解決辦法的優(yōu)缺點(diǎn)。
其實(shí)這篇文章的例子更適合用 mutex,因?yàn)檫@樣代碼更加清晰。
最后,請(qǐng)毫無(wú)顧忌的指出我的錯(cuò)誤!
標(biāo)簽:九江 運(yùn)城 本溪 晉城 湘潭 喀什 楚雄 深圳
巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Go語(yǔ)言并發(fā)模型的2種編程方案》,本文關(guān)鍵詞 語(yǔ)言,并發(fā),模型,的,2種,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。