咱們都知道Golang併發優選channel,但channel不是萬能的,Golang爲咱們提供了另外一種選擇:sync。經過這篇文章,你會了解sync包最基礎、最經常使用的方法,至於sync和channel之爭留給下一篇文章。git
sync包提供了基礎的異步操做方法,好比互斥鎖(Mutex)、單次執行(Once)和等待組(WaitGroup),這些異步操做主要是爲低級庫提供,上層的異步/併發操做最好選用通道和通訊。github
sync包提供了:golang
這篇文章是sync包的入門文章,因此只介紹經常使用的結構和方法:Mutex
、RWMutex
、WaitGroup
、Once
,而Cond
、Pool
和Map
留給你們自行探索,或有需求再介紹。bash
常作併發工做的朋友對互斥鎖應該不陌生,Golang裏互斥鎖須要確保的是某段時間內,不能有多個協程同時訪問一段代碼(臨界區)。微信
互斥鎖被稱爲Mutex
,它有2個函數,Lock()
和Unlock()
分別是獲取鎖和釋放鎖,以下:併發
type Mutex func (m *Mutex) Lock(){} func (m *Mutex) Unlock(){}
Mutex
的初始值爲未鎖的狀態,而且Mutex
一般做爲結構體的匿名成員存在。less
通過了上面這麼「官方」的介紹,舉個例子:你在工商銀行有100元存款,這張卡綁定了支付寶和微信,在中午12點你用支付寶支付外賣30元,你在微信發紅包,搶到10塊。銀行須要按順序執行上面兩件事,先減30再加10或者先加10再減30,結果都是80,但若是同時執行,結果多是,只減了30或者只加了10,即你有70元或者你有110元。前一個結果是你賠了,後一個結果是銀行賠了,銀行可不但願把這種事算錯。異步
看看實際使用吧:建立一個銀行,銀行裏存每一個帳戶的錢,存儲查詢都加了鎖操做,這樣銀行就不會算錯帳了。
銀行的定義:函數
type Bank struct { sync.Mutex saving map[string]int // 每帳戶的存款金額 } func NewBank() *Bank { b := &Bank{ saving: make(map[string]int), } return b }
銀行的存取錢:測試
// Deposit 存款 func (b *Bank) Deposit(name string, amount int) { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { b.saving[name] = 0 } b.saving[name] += amount } // Withdraw 取款,返回實際取到的金額 func (b *Bank) Withdraw(name string, amount int) int { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { return 0 } if b.saving[name] < amount { amount = b.saving[name] } b.saving[name] -= amount return amount } // Query 查詢餘額 func (b *Bank) Query(name string) int { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { return 0 } return b.saving[name] }
模擬操做:小米支付寶存了100,而且同時花了20。
func main() { b := NewBank() go b.Deposit("xiaoming", 100) go b.Withdraw("xiaoming", 20) go b.Deposit("xiaogang", 2000) time.Sleep(time.Second) fmt.Printf("xiaoming has: %d\n", b.Query("xiaoming")) fmt.Printf("xiaogang has: %d\n", b.Query("xiaogang")) }
結果:先存後花。
➜ sync_pkg git:(master) ✗ go run mutex.go xiaoming has: 80 xiaogang has: 2000
也多是:先花後存,由於先花20,由於小明沒錢,因此沒花出去。
➜ sync_pkg git:(master) ✗ go run mutex.go xiaoming has: 100 xiaogang has: 2000
這個例子只是介紹了mutex的基本使用,若是你想多研究下mutex,那就去個人Github(閱讀原文)下載下來代碼,本身修改測試。Github中還提供了沒有鎖的例子,運行屢次總能碰到錯誤:
fatal error: concurrent map writes
這是因爲併發訪問map形成的。
讀寫鎖是互斥鎖的特殊變種,若是是計算機基本知識紮實的朋友會知道,讀寫鎖來自於讀者和寫者的問題,這個問題就不介紹了,介紹下咱們的重點:讀寫鎖要達到的效果是同一時間能夠容許多個協程讀數據,但只能有且只有1個協程寫數據。
也就是說,讀和寫是互斥的,寫和寫也是互斥的,但讀和讀並不互斥。具體講,當有至少1個協程讀時,若是須要進行寫,就必須等待全部已經在讀的協程結束讀操做,寫操做的協程纔得到鎖進行寫數據。當寫數據的協程已經在進行時,有其餘協程須要進行讀或者寫,就必須等待已經在寫的協程結束寫操做。
讀寫鎖是RWMutex
,它有5個函數,它須要爲讀操做和寫操做分別提供鎖操做,這樣就4個了:
Lock()
和Unlock()
是給寫操做用的。RLock()
和RUnlock()
是給讀操做用的。RLocker()
能獲取讀鎖,而後傳遞給其餘協程使用。使用較少。
type RWMutex func (rw *RWMutex) Lock(){} func (rw *RWMutex) RLock(){} func (rw *RWMutex) RLocker() Locker{} func (rw *RWMutex) RUnlock(){} func (rw *RWMutex) Unlock(){}
上面的銀行實現不合理:你們都是拿手機APP查餘額,能夠同時幾我的一塊兒查呀,這根本不影響,銀行的鎖能夠換成讀寫鎖。存、取錢是寫操做,查詢金額是讀操做,代碼修改以下,其餘不變:
type Bank struct { sync.RWMutex saving map[string]int // 每帳戶的存款金額 } // Query 查詢餘額 func (b *Bank) Query(name string) int { b.RLock() defer b.RUnlock() if _, ok := b.saving[name]; !ok { return 0 } return b.saving[name] } func main() { b := NewBank() go b.Deposit("xiaoming", 100) go b.Withdraw("xiaoming", 20) go b.Deposit("xiaogang", 2000) time.Sleep(time.Second) print := func(name string) { fmt.Printf("%s has: %d\n", name, b.Query(name)) } nameList := []string{"xiaoming", "xiaogang", "xiaohong", "xiaozhang"} for _, name := range nameList { go print(name) } time.Sleep(time.Second) }
結果,可能不同,由於協程都是併發執行的,執行順序不固定:
➜ sync_pkg git:(master) ✗ go run rwmutex.go xiaohong has: 0 xiaozhang has: 0 xiaogang has: 2000 xiaoming has: 100
互斥鎖和讀寫鎖大多數人可能比較熟悉,而對等待組(WaitGroup
)可能就不那麼熟悉,甚至有點陌生,因此先來介紹下等待組在現實中的例子。
大家團隊有5我的,你做爲隊長要帶領你們打開藏有寶藏的箱子,但這個箱子須要4把鑰匙才能同時打開,你把尋找4把鑰匙的任務,分配給4個隊員,讓他們分別去尋找,而你則守着寶箱,在這等待,等他們都找到回來後,一塊兒插進鑰匙打開寶箱。
這其中有個很重要的過程叫等待:等待一些工做完成後,再進行下一步的工做。若是使用Golang實現,就得使用等待組。
等待組是WaitGroup
,它有3個函數:
Add()
:在被等待的協程啓動前加1,表明要等待1個協程。Done()
:被等待的協程執行Done,表明該協程已經完成任務,通知等待協程。Wait()
: 等待其餘協程的協程,使用Wait進行等待。type WaitGroup func (wg *WaitGroup) Add(delta int){} func (wg *WaitGroup) Done(){} func (wg *WaitGroup) Wait(){}
來,一塊兒看下怎麼用WaitGroup實現上面的問題。
隊長先建立一個WaitGroup對象wg,每一個隊員都是1個協程, 隊長讓隊員出發前,使用wg.Add()
,隊員出發尋找鑰匙,隊長使用wg.Wait()
等待(阻塞)全部隊員完成,某個隊員完成時執行wg.Done()
,等全部隊員找到鑰匙,wg.Wait()
則返回,完成了等待的過程,接下來就是開箱。
結合以前的協程池的例子,修改爲WG等待協程池協程退出,實例代碼:
func leader() { var wg sync.WaitGroup wg.Add(4) for i := 0; i < 4; i++ { go follower(&wg, i) } wg.Wait() fmt.Println("open the box together") } func follower(wg *sync.WaitGroup, id int) { fmt.Printf("follwer %d find key\n", id) wg.Done() }
結果:
➜ sync_pkg git:(master) ✗ go run waitgroup.go follwer 3 find key follwer 1 find key follwer 0 find key follwer 2 find key open the box together
WaitGroup也經常使用在協程池的處理上,協程池等待全部協程退出,把上篇文章《Golang併發模型:輕鬆入門協程池》的例子改下:
func workerPool(n int, jobCh <-chan int, retCh chan<- string) { var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go worker(&wg, i, jobCh, retCh) } wg.Wait() close(retCh) } func worker(wg *sync.WaitGroup, id int, jobCh <-chan int, retCh chan<- string) { cnt := 0 for job := range jobCh { cnt++ ret := fmt.Sprintf("worker %d processed job: %d, it's the %dth processed by me.", id, job, cnt) retCh <- ret } wg.Done() }
在程序執行前,一般須要作一些初始化操做,但觸發初始化操做的地方是有多處的,可是這個初始化又只能執行1次,怎麼辦呢?
使用Once就能輕鬆解決,once
對象是用來存放1個無入參無返回值的函數,once能夠確保這個函數只被執行1次。
type Once func (o *Once) Do(f func()){}
直接把官方代碼給你們搬過來看下,once在10個協程中調用,但once中的函數onceBody()
只執行了1次:
package main import ( "fmt" "sync" ) func main() { var once sync.Once onceBody := func() { fmt.Println("Only once") } done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } for i := 0; i < 10; i++ { <-done } }
結果:
➜ sync_pkg git:(master) ✗ go run once.go Only once
本文全部示例源碼,及歷史文章、代碼都存儲在Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/sync_pkg
此次先介紹入門的知識,下次再介紹一些深刻思考、最佳實踐,不能一口吃個胖子,我們慢慢來,順序漸進。
下一篇我以這些主題進行介紹,歡迎關注:
- 若是這篇文章對你有幫助,請點個贊/喜歡,感謝。
- 本文做者:大彬
- 若是喜歡本文,隨意轉載,但請保留此原文連接:http://lessisbetter.site/2019/01/04/golang-pkg-sync/