Go 学习笔记(23)— 并发(02)[竞争,锁资源,原子函数sync/atomic、互斥锁sync.Mutex]
本文參考 《Go 語言實(shí)戰(zhàn)》
1. 競(jìng)爭(zhēng)狀態(tài)簡(jiǎn)述
如果兩個(gè)或者多個(gè) goroutine 在沒有互相同步的情況下,訪問某個(gè)共享的資源,并試圖同時(shí)讀和寫這個(gè)資源,就處于相互競(jìng)爭(zhēng)的狀態(tài),這種情況被稱作競(jìng)爭(zhēng)狀態(tài)(race candition)。
對(duì)一個(gè)共享資源的讀和寫操作必須是原子化的,換句話說,同一時(shí)刻只能有一個(gè) goroutine 對(duì)共享資源進(jìn)行讀和寫操作。
當(dāng)某些東西被認(rèn)為是原子的,或者具有原子性的時(shí)候,這意味著在它運(yùn)行的環(huán)境中,它是不可分割的或不可中斷的。
// 這個(gè)示例程序展示如何在程序里造成競(jìng)爭(zhēng)狀態(tài)
// 實(shí)際上不希望出現(xiàn)這種情況
package mainimport ("fmt""runtime""sync"
)var (// counter是所有g(shù)oroutine都要增加其值的變量counter int// wg用來等待程序結(jié)束wg sync.WaitGroup
)// main是所有Go程序的入口
func main() {// 計(jì)數(shù)加2,表示要等待兩個(gè)goroutinewg.Add(2)// 創(chuàng)建兩個(gè)goroutinego incCounter(1)go incCounter(2)// 等待goroutine結(jié)束wg.Wait()fmt.Println("Final Counter:", counter)
}// incCounter增加包里counter變量的值
func incCounter(id int) {// 在函數(shù)退出時(shí)調(diào)用Done來通知main函數(shù)工作已經(jīng)完成defer wg.Done()for count := 0; count < 2; count++ {// 捕獲counter的值value := counter// 當(dāng)前goroutine從線程退出,并放回到隊(duì)列/*用于將 goroutine 從當(dāng)前線程退出,給其他 goroutine 運(yùn)行的機(jī)會(huì)。在兩次操作中間這樣做的目的是強(qiáng)制調(diào)度器切換兩個(gè) goroutine,以便讓競(jìng)爭(zhēng)狀態(tài)的效果變得更明顯。*/runtime.Gosched()// 增加本地value變量的值value++// 將該值保存回countercounter = value}
}
輸出:
Final Counter: 2
變量 counter 會(huì)進(jìn)行 4 次讀和寫操作,每個(gè) goroutine 執(zhí)行兩次。但是,程序終止時(shí), counter 變量的值為2。
每個(gè) goroutine 都會(huì)覆蓋另一個(gè) goroutine 的工作。這種覆蓋發(fā)生在 goroutine 切換的時(shí)候。每個(gè) goroutine 創(chuàng)造了一個(gè) counter 變量的副本,之后就切換到另一個(gè) goroutine 。
當(dāng)這個(gè) goroutine 再次運(yùn)行的時(shí)候, counter 變量的值已經(jīng)改變了,但是 goroutine 并沒有更新自己的那個(gè)副本的值,而是繼續(xù)使用這個(gè)副本的值,用這個(gè)值遞增,并存回 counter 變量,結(jié)果覆蓋了另一個(gè) goroutine 完成的工作。
圖: 競(jìng)爭(zhēng)狀態(tài)下程序行為的圖像表達(dá)
2. 鎖住共享資源
Go 語言提供了傳統(tǒng)的同步 goroutine 的機(jī)制,就是對(duì)共享資源加鎖。如果需要順序訪問一個(gè)整型變量或者一段代碼, atomic 和 sync 包里的函數(shù)提供了很好的解決方案。
下面我們了解一下 atomic 包里的幾個(gè)函數(shù)以及 sync 包里的 mutex 類型。
2.1 原子函數(shù)
原子函數(shù)能夠以很底層的加鎖機(jī)制來同步訪問整型變量和指針.
示例 1:
package mainimport ("fmt""sync/atomic"
)var (// 序列號(hào)seq int64
)// 序列號(hào)生成器
func GenID() int64 {// 嘗試原子的增加序列號(hào)// 這里故意沒有使用 atomic .Addlnt64()的返回值作 為 GenID () 函數(shù)的返// 回值,因此會(huì)造成一個(gè)競(jìng)態(tài)問題atomic.AddInt64(&seq, 1)return seq
}func main() {// 10個(gè)并發(fā)序列號(hào)生成for i := 0; i < 10; i++ {go GenID()}fmt.Println(GenID())
}
在運(yùn)行程序時(shí),為運(yùn)行參數(shù)加入 -race 參數(shù),開啟運(yùn)行時(shí)( runtime )對(duì)競(jìng)態(tài)問題的分析,命令如下:
wohu@wohu-dev:~/gocode/src$ go run -race temp.go
==================
WARNING: DATA RACE
Write at 0x0000005f8178 by goroutine 8:sync/atomic.AddInt64()/usr/local/go/src/runtime/race_amd64.s:276 +0xbmain.GenID()/home/wohu/gocode/src/temp.go:16 +0x43Previous read at 0x0000005f8178 by goroutine 7:main.GenID()/home/wohu/gocode/src/temp.go:17 +0x53Goroutine 8 (running) created at:main.main()/home/wohu/gocode/src/temp.go:23 +0x4fGoroutine 7 (finished) created at:main.main()/home/wohu/gocode/src/temp.go:23 +0x4f
==================
4
Found 1 data race(s)
exit status 66
修改該函數(shù)為下面即可正常。
// 序列號(hào)生成器
func GenID() int64 {// 嘗試原子的增加序列號(hào)return atomic.AddInt64(&seq, 1)
}
示例代碼 2:
// 這個(gè)示例程序展示如何使用atomic包來提供
// 對(duì)數(shù)值類型的安全訪問
package mainimport ("fmt""runtime""sync""sync/atomic"
)var (// counter是所有g(shù)oroutine都要增加其值的變量counter int32// wg用來等待程序結(jié)束wg sync.WaitGroup
)// main是所有Go程序的入口
func main() {// 計(jì)數(shù)加2,表示要等待兩個(gè)goroutinewg.Add(2)// 創(chuàng)建兩個(gè)goroutinego incCounter(1)go incCounter(2)// 等待goroutine結(jié)束wg.Wait()fmt.Println("Final Counter:", counter)
}// incCounter增加包里counter變量的值
func incCounter(id int) {// 在函數(shù)退出時(shí)調(diào)用Done來通知main函數(shù)工作已經(jīng)完成defer wg.Done()for count := 0; count < 2; count++ {// 安全地對(duì)counter加1atomic.AddInt32(&counter, 1)// 當(dāng)前goroutine從線程退出,并放回到隊(duì)列/*用于將 goroutine 從當(dāng)前線程退出,給其他 goroutine 運(yùn)行的機(jī)會(huì)。在兩次操作中間這樣做的目的是強(qiáng)制調(diào)度器切換兩個(gè) goroutine,以便讓競(jìng)爭(zhēng)狀態(tài)的效果變得更明顯。*/runtime.Gosched()}
}
輸出:
Final Counter: 4
程序的第43行使用了 atmoic 包的 AddInt64 函數(shù)。這個(gè)函數(shù)會(huì)同步整型值的加法,方法是強(qiáng)制同一時(shí)刻只能有一個(gè) goroutine 運(yùn)行并完成這個(gè)加法操作。
當(dāng) goroutine 試圖去調(diào)用任何原子函數(shù)時(shí),這些 goroutine 都會(huì)自動(dòng)根據(jù)所引用的變量做同步處理。
另外兩個(gè)有用的原子函數(shù)是 LoadInt64 和 StoreInt64 。這兩個(gè)函數(shù)提供了一種安全地讀和寫一個(gè)整型值的方式。
如下代碼示例程序使用 LoadInt64 和 StoreInt64 來創(chuàng)建一個(gè)同步標(biāo)志,這個(gè)標(biāo)志可以向程序里多個(gè) goroutine 通知某個(gè)特殊狀態(tài)。
// 這個(gè)示例程序展示如何使用atomic包里的
// Store和Load類函數(shù)來提供對(duì)數(shù)值類型
// 的安全訪問
package mainimport ("fmt""sync""sync/atomic""time"
)var (// shutdown是通知正在執(zhí)行的goroutine停止工作的標(biāo)志shutdown int64// wg用來等待程序結(jié)束wg sync.WaitGroup
)// main是所有Go程序的入口
func main() {// 計(jì)數(shù)加2,表示要等待兩個(gè)goroutinewg.Add(2)// 創(chuàng)建兩個(gè)goroutinego doWork("A")go doWork("B")// 給定goroutine執(zhí)行的時(shí)間time.Sleep(1 * time.Second)// 該停止工作了,安全地設(shè)置shutdown標(biāo)志fmt.Println("Shutdown Now")atomic.StoreInt64(&shutdown, 1)// 等待goroutine結(jié)束wg.Wait()
}// doWork用來模擬執(zhí)行工作的goroutine,
// 檢測(cè)之前的shutdown標(biāo)志來決定是否提前終止
func doWork(name string) {// 在函數(shù)退出時(shí)調(diào)用Done來通知main函數(shù)工作已經(jīng)完成defer wg.Done()for {fmt.Printf("Doing %s Work\n", name)time.Sleep(250 * time.Millisecond)// 要停止工作了嗎?if atomic.LoadInt64(&shutdown) == 1 {fmt.Printf("Shutting %s Down\n", name)break}}
}
2.2 互斥鎖
另一種同步訪問共享資源的方式是使用互斥鎖( mutex )。互斥鎖這個(gè)名字來自互斥(mutual exclusion)的概念。
互斥鎖用于在代碼上創(chuàng)建一個(gè)臨界區(qū),保證同一時(shí)間只有一個(gè) goroutine 可以執(zhí)行這個(gè)臨界區(qū)代碼。
// 這個(gè)示例程序展示如何使用互斥鎖來
// 定義一段需要同步訪問的代碼臨界區(qū)
// 資源的同步訪問
package mainimport ("fmt""runtime""sync"
)var (// counter是所有g(shù)oroutine都要增加其值的變量counter int// wg用來等待程序結(jié)束wg sync.WaitGroup// mutex 用來定義一段代碼臨界區(qū)mutex sync.Mutex
)// main是所有Go程序的入口
func main() {// 計(jì)數(shù)加2,表示要等待兩個(gè)goroutinewg.Add(2)// 創(chuàng)建兩個(gè)goroutinego incCounter(1)go incCounter(2)// 等待goroutine結(jié)束wg.Wait()fmt.Printf("Final Counter: %d\n", counter)
}// incCounter使用互斥鎖來同步并保證安全訪問,
// 增加包里counter變量的值
func incCounter(id int) {// 在函數(shù)退出時(shí)調(diào)用Done來通知main函數(shù)工作已經(jīng)完成defer wg.Done()for count := 0; count < 2; count++ {// 同一時(shí)刻只允許一個(gè)goroutine進(jìn)入// 這個(gè)臨界區(qū)mutex.Lock(){ // 使用大括號(hào)只是為了讓臨界區(qū)看起來更清晰,并不是必需的。// 捕獲counter的值value := counter// 當(dāng)前goroutine從線程退出,并放回到隊(duì)列runtime.Gosched()// 增加本地value變量的值value++// 將該值保存回countercounter = value}mutex.Unlock()// 釋放鎖,允許其他正在等待的goroutine// 進(jìn)入臨界區(qū)}
}
對(duì) counter 變量的操作在第 46 行和第 60 行的 Lock() 和 Unlock() 函數(shù)調(diào)用定義的臨界區(qū)里被保護(hù)起來。
同一時(shí)刻只有一個(gè) goroutine 可以進(jìn)入臨界區(qū)。之后,直到調(diào)用 Unlock() 函數(shù)之后,其他 goroutine 才能進(jìn)入臨界區(qū)。當(dāng)?shù)?52 行強(qiáng)制將當(dāng)前 goroutine 退出當(dāng)前線程后,調(diào)度器會(huì)再次分配這個(gè) goroutine 繼續(xù)運(yùn)行。當(dāng)程序結(jié)束時(shí),我們得到正確的值 4,競(jìng)爭(zhēng)狀態(tài)不再存在。
2.3 讀寫互斥鎖 sync.RWMutex
在讀多寫少的環(huán)境中,可以優(yōu)先使用讀寫互斥鎖, sync 包中的 RWMutex 提供了讀寫互斥鎖的封裝。
package mainimport ("fmt""sync""time"
)var (count int// 變量對(duì)應(yīng)的讀寫互斥鎖countGuard sync.RWMutex
)func GetCount() int {countGuard.RLock()defer countGuard.RUnlock()return count
}func SetCount(c int) {countGuard.Lock(){count += c}countGuard.Unlock()
}func main() {// 可以進(jìn)行并發(fā)安全的設(shè)置for i := 0; i < 10; i++ {go SetCount(2)}time.Sleep(2 * time.Second)// 可以進(jìn)行并發(fā)安全的讀取fmt.Println(GetCount())
}
總結(jié)
以上是生活随笔為你收集整理的Go 学习笔记(23)— 并发(02)[竞争,锁资源,原子函数sync/atomic、互斥锁sync.Mutex]的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 女性为什么会患卵巢早衰
- 下一篇: 爱情公寓大电影百度云资源可不可以发一下,