盤點Go并發那些事兒之二-GO并發控制得心應手
盤點Golang并發那些事兒之二
上一節提到,golang中直接使用關鍵字go創建goroutine,無法滿足我們的需求。主要問題如下
無法有效的并發執行完成
無法有效的控制并發
首先我們再來看看下面這個栗子,代碼如下
1//?example-goroutine?anonymous
2
3package?main
4
5import?(
6????"fmt"
7????"time"
8)
9
10func?anonymous1()?{
11????startTime?:=?time.Now()
12????//?創建十個goroutine
13????for?i?:=?0;?i?10;?i++?{
14????????go?func()?{
15????????????fmt.Println("HelloWord~,?stamp?is",?i)
16????????}()
17????}
18????fmt.Println("Main~")
19????spendTime?:=?time.Since(startTime)
20????fmt.Println("Spend?Time:",?spendTime)
21????//?防止goroutine提前退出
22????//?time.Sleep(time.Second)
23}
24
25//?goroutine?anonymous
26func?main()?{
27????anonymous2()
28}
此時你會發現有些任務被多次執行了,但有些任務卻又沒有被執行。以上例子雖加速了運行,但帶來的損失卻也是巨大的。例如銀行轉賬等,一旦出現以上情況多次付款也隨之而來了。弊大于利
首先我們來分析以上代碼,為什么會出現此種情況?雖然是個廢品,但也是俺辛辛苦苦的寫的不是,讓俺做個明白鬼。
我們從里面往外分析anonymous1首先他是個匿名函數 + 立即執行函數,且變量i并不是傳遞的參數,而是外部循環帶進來的。由上圖,我們知道,執行流程為先創建goroutine,執行邏輯,返回結果。
請思考:
goroutine,越多越好么?為什么
如何避免以上情景?如何避免提前退出?
信道-Channel
信道的英文是channel,在golang當中的關鍵字是chan。它的用途是用來在goroutine之間傳輸數據,這里你可能要問了,為什么一定得是goroutine之間傳輸數據呢,函數之間傳遞不行嗎?
因為正常的傳輸數據直接以參數的形式傳遞就可以了,只有在并發場景當中,多個線程彼此隔離的情況下,才需要一個特殊的結構傳輸數據。
Go語言的并發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信。
如果說goroutine是Go程序并發的執行體,channel就是它們之間的連接。channel是可以讓一個goroutine發送特定值到另一個goroutine的通信機制。
Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要為其指定元素類型。
channel底層的實現為互斥鎖
example
1var?變量?chan?元素類型
2//?example-var
3//?只聲明
4var?a?chan?int
5var?b?chan?string
6var?c?chan?byte
7var?d?chan?[]string
8var?e?chan?[]int
9//?實例化
10a?=?make(chan?[]int)
11
12//example-2(推薦使用)
13管道名稱?:=?make(chan?數據類型?[緩沖區size])
無緩沖channel
示例代碼如下
1package?main
2
3import?(
4????"fmt"
5????"time"
6)
7
8func?hello(intCh?<-chan?int)?{
9????fmt.Println("Hello,?Gopher.?I?am?stamp[Id]",?<-intCh)
10????time.Sleep(time.Second?*?2)
11}
12
13func?main()?{
14????startTime?:=?time.Now()
15????const?jobNumber?=?100?*?100
16????//?create?chan
17????intCh?:=?make(chan?int)
18????for?i?:=?0;?i?<=?jobNumber;?i++?{
19????????//?create?goroutine?same?number?for?jobNumber
20????????go?hello(intCh)
21????????intCh?<-?i
22????}
23????fmt.Println("Completed,?Spend?Time?:",?time.Since(startTime))
24}
這速度可謂是非常的快啊
帶緩沖Channel
帶緩沖的 channel(buffered channel) 是一種在被接收前能存儲一個或者多個值的通道。這種類型的通道并不強制要求 goroutine 之間必須同時完成發送和接收。通道會阻塞發送和接收動作的條件也會不同。只有在通道中沒有要接收的值時,接收動作才會阻塞。只有在通道沒有可用緩沖區容納被發送的值時,發送動作才會阻塞。這導致有緩沖的通道和無緩沖的通道之間的一個很大的不同:
無緩沖的通道保證進行發送和接收的 goroutine 會在同一時間進行數據交換;有緩沖的通道沒有這種保證
來段代碼壓壓驚
1package?main
2
3import?(
4????"fmt"
5????"time"
6)
7
8func?hello(intCh?<-chan?int)?{
9????fmt.Println("Hello,?Gopher.?I?am?stamp[Id]",?<-intCh)
10????time.Sleep(time.Second?*?2)
11}
12
13func?hello1(intCh?<-chan?int)?{
14????fmt.Println("Hello,?Gopher1.?I?am?stamp[Id]",?<-intCh)
15????time.Sleep(time.Second?*?2)
16}
17
18func?main()?{
19????startTime?:=?time.Now()
20????const?jobNumber?=?100?*?100
21????//?create?chan
22????intCh?:=?make(chan?int,?100)
23????for?i?:=?0;?i?<=?jobNumber;?i++?{
24????????//?create?goroutine?same?number?for?jobNumber
25????????go?hello(intCh)
26????????go?hello1(intCh)
27????????intCh?<-?i
28????}
29????fmt.Println("Completed,?Spend?Time?:",?time.Since(startTime))
30}
運行效果如下
這速度杠杠滴哈,別急,同時也讓我和你說執行流程,老規矩,上圖
首先開始執行把需要傳遞的數據打到channle里面,然后goroutine去取,執行。那么有留下幾個問題
還可以加速么?
加速的方法?
可能帶來什么新的問題?
如何解決?
單向 channel
有時候,我們有一些特殊的業務需求,比如限制一個 channel 只可以接收但是不能發送,或者限制一個 channel 只能發送但不能接收,這種 channel 稱為單向 channel。
單向 channel 的聲明也很簡單,只需要在聲明的時候帶上 <- 操作符即可,如下面的代碼所示:
1onlySend?:=?make(chan<-?int)
2onlyReceive:=make(<-chan?int)
使用單向 channel 的較多場景一般在函數參數中使用較多,這樣可以防止一些操作影響了 channel。
1//example?channel
2onlySend?:=?make(chan<-?int)
3onlyReceive:=make(<-chan?int)
4
5//example?function
6package?main
7
8import?"fmt"
9
10func?exs(accept?<-chan?int,?recipient?chan<-?int)?{
11????for?result?:=?range?accept?{
12????????fmt.Println("Received?only?sent?channel?a:",?result)
13????????recipient?<-?result?+?2
14????}
15
16????//fmt.Println("Send?Only",?recipient)
17}
18
19func?main()?{
20????const?processNumber?=?100
21????sender?:=?make(chan?int,?processNumber)
22????recipient?:=?make(chan?int,?processNumber)
23????for?e?:=?0;?e?10;?e++?{
24????????go?exs(sender,?recipient)
25????}
26
27????for?s?:=?0;?s?
28????????sender?<-?s
29????}
30????for?r?:=?0;?r?
31????????//<-recipient
32????????fmt.Println("recipient",?<-recipient)
33????}
34}
小技巧:箭頭該誰指誰?這可把我整的不好了,別慌,我告訴你,到底該誰指誰。其實很簡單
箭頭一致向左指
1a?<-?chan?<-?b
2
3//?存入
4chan?<-?b
5
6//?取出
7a?:=?<-?chan
Chan其實就是起到一個中間人的作用,箭頭指向chan,那就是放入,chan指出去 就是拿出來。
相信你應該記住了吧,反正我記住了
多路復用Channel
假設要從網上下載一個文件,啟動了 5個 goroutine 進行下載,并把結果發送到 5 個 channel 中。其中,哪個先下載好,就會使用哪個 channel 的結果。
在這種情況下,如果我們嘗試獲取第一個 channel 的結果,程序就會被阻塞,無法獲取剩下4個 channel 的結果,也無法判斷哪個先下載好。這個時候就需要用到多路復用操作了,在 Go 語言中,通過 select 語句可以實現多路復用,其語句格式如下:
1select{
2????case?<-ch1:
3????????...
4????case?data?:=?<-ch2:
5????????...
6????case?ch3<-data:
7????????...
8????default:
9????????默認操作
10}
整體結構和 switch 非常像,都有 case 和 default,只不過 select 的 case 是一個個可以操作的 channel。
小提示:多路復用可以簡單地理解為,N 個 channel 中,任意一個 channel 有數據產生,select 都可以監聽到,然后執行相應的分
支,接收數據并處理。
使用select語句能提高代碼的可讀性。
可處理一個或多個channel的發送/接收操作。
如果多個case同時滿足,select會隨機選擇一個。
對于沒有case的select{}會一直等待,可用于阻塞main函數。
1//?example?select
2package?main
3
4import?(
5????"fmt"
6????"time"
7)
8
9func?main()?{
10
11????c1?:=?make(chan?string)
12????c2?:=?make(chan?string)
13
14????go?func()?{
15????????time.Sleep(1?*?time.Second)
16????????c1?<-?"one"
17????}()
18????go?func()?{
19????????time.Sleep(2?*?time.Second)
20????????c2?<-?"two"
21????}()
22
23????for?i?:=?0;?i?2;?i++?{
24????????select?{
25????????case?msg1?:=?<-c1:
26????????????fmt.Println("received",?msg1)
27????????case?msg2?:=?<-c2:
28????????????fmt.Println("received",?msg2)
29????????}
30????}
31}
小結:關于數據流動、傳遞等情況的優先使用channle, 它是并發安全的,且性能優異
Sync深入并發控制
sync.waitGroup
在此之前我們先去,解決一個開啟goroutine,提前退出的例子
示例代碼如下
1package?main
2
3import?(
4????"fmt"
5????"sync"
6????//"time"
7)
8
9var?wg?sync.WaitGroup
10func?main()?{
11????for?i?:=?0;?i?10?;?i++?{
12????????go?exampleOut(i)
13????}
14}
15
16func?exampleOut(i?int)??{
17????fmt.Println("Hello,?Gopher,?I?am?[Id]",?i)
18}
仔細看,你會發現根本就沒有輸出,原因是它開啟goroutine,也需要時間。main函數并會等待,當然我們也可以手動添加一個停止,但這個并不能有效的阻止(你我都知道需要多久才能把goroutine執行完成),那有沒有辦法。。。
答案當然是有滴,它就是sync.WaitGroup
WaitGroup等待goroutine的集合完成。主goroutine調用添加以設置要等待的goroutine的數量。然后,每個goroutine都會運行并在完成后調用Done。同時,可以使用Wait來阻塞,直到所有goroutine完成。
你可以理解為計數器
1//?`sync.WaitGroup`一共有三個方法,他們分別是:
2Add(delta?int)
3//Add將可能為負數的增量添加到WaitGroup計數器中。如果計數器為零,則釋放等待時阻塞的所有goroutine
4Done()
5//?完成將WaitGroup計數器減一。
6?Wait()
7//?等待塊,直到WaitGroup計數器為零。
example
1//?WaitGroup
2package?main
3
4import?(
5????"fmt"
6????"sync"
7)
8
9//?聲明WaitGroup
10var?wg?sync.WaitGroup
11
12func?main()?{
13????for?i?:=?0;?i?10;?i++?{
14????????//?WaitGroup?計數器?+?1
15????????//?其delta為你開啟的`groutine`數量
16????????wg.Add(1)
17????????go?exampleOut(i)
18????}
19????//?等待?WaitGroup?計數器為0
20????wg.Wait()
21}
22
23func?exampleOut(i?int)?{
24????//?WaitGroup?計數器?-?1
25????wg.Done()
26????fmt.Println("Hello,?Gopher,?I?am?[Id]",?i)
27}
sync.Mutex
無論是前面的channle還是sync都是為了干一件事,那就是并發控制,也許你也和我一樣有以下幾個問題
我們為什么需要并發控制,不要可以么?
并發控制到底是控制什么?
并發控制有哪幾種方案,他們分別適用于哪種場景?
如何做好并發控制呢?
以上幾點就是我們此節需要了解、以及解決的問題
首先解決我們一起探究第一個問題,為什么需要并發控制?
首先有這么一個問題、以及相關的解決措施,絕對不是脫褲子放屁,多此一舉。需要并發控制的原因有很多,總結一句話那就是資源競爭
資源競爭
在一個 goroutine 中,如果分配的內存沒有被其他 goroutine 訪問,只在該 goroutine 中被使用,那么不存在資源競爭的問題。
但如果同一塊內存被多個 goroutine 同時訪問,就會產生不知道誰先訪問也無法預料最后結果的情況。這就是資源競爭,這塊內存可以稱為共享的資源
還記得在channel中,我講到 Go語言的并發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信,這點尤為重要需要我們去記住與掌握
首先我們來看一個累加求和的例子,代碼如下所示
1package?main
2
3import?(
4????"fmt"
5????"sync"
6)
7
8var?(
9????x?int64
10????wg?sync.WaitGroup
11)
12
13func?add()?{
14????for?i?:=?0;?i?5000;?i++?{
15????????x?=?x?+?1
16????}
17????wg.Done()
18}
19func?main()?{
20????wg.Add(5)
21????go?add()
22????go?add()
23????go?add()
24????go?add()
25????go?add()
26????wg.Wait()
27????fmt.Println(x)
28}
期待輸出值為25000,sum + 10 加和 5000次,執行五次,我們口算答案是5000,可輸出結果卻是3048,而且每次答案還不一樣。好家伙
這是為什么呢?,靚仔疑惑~
其根本的原因就是資源惡意競爭
精囊妙計:
使用 go build、go run、go test 這些 Go 語言工具鏈提供的命令時,添加 -race 標識可以幫你檢查 Go 語言代碼是否存在資源競爭。
1//?example
2go?run?-race?demo3.go
那么該怎么解決呢?
sync.Mutex互斥鎖,顧名思義,指的是在同一時刻只有一個goroutine執行某段代碼,其他goroutine都要等待該goroutine執行完畢后才能繼續執行。
在下面的示例中,我聲明了一個互斥鎖 mutex,然后修改 add 函數,對 sum+=i 這段代碼加鎖保護。這樣這段訪問共享資源的代碼片段就并發安全了,可以得到正確的結果
sync.Mutex為我們提供了兩個方法,加鎖與解鎖,修改時先獲取鎖,修改后釋放鎖
代碼修改如下
1package?main
2
3import?(
4????"fmt"
5????"sync"
6)
7
8var?(
9????x????int64
10????lock?sync.Mutex
11????wg???sync.WaitGroup
12)
13
14func?add()?{
15????for?i?:=?0;?i?1000;?i++?{
16????????lock.Lock()?//?加鎖
17????????x?+=?1
18????????lock.Unlock()?//?解鎖
19????}
20????wg.Done()
21}
22func?main()?{
23????wg.Add(5)
24????go?add()
25????go?add()
26????go?add()
27????go?add()
28????go?add()
29????wg.Wait()
30????fmt.Println(x)
31}
女少啊~
在以上示例代碼中x += 1,部分被稱之為臨界區
在同步的程序設計中,臨界區段指的是一個訪問共享資源的程序片段,而這些共享資源又有無法同時被多個goroutine訪問的特性。 當有協程進入臨界區段時,其他協程必須等待,這樣就保證了臨界區的并發安全。
sync.RWMutex
互斥鎖是完全互斥的,但是有很多實際的場景下是讀多寫少的,當我們并發的去讀取一個資源不涉及資源修改的時候是沒有必要加鎖的,這種場景下使用讀寫鎖是更好的一種選擇。讀寫鎖在Go語言中使用sync包中的RWMutex類型。
讀寫鎖分為兩種:讀鎖和寫鎖。當一個goroutine獲取讀鎖之后,其他的goroutine如果是獲取讀鎖會繼續獲得鎖,如果是獲取寫鎖就會等待;當一個goroutine獲取寫鎖之后,其他的goroutine無論是獲取讀鎖還是寫鎖都會等待。
1var?(
2????x??????int64
3????wg?????sync.WaitGroup
4????lock???sync.Mutex
5????rwlock?sync.RWMutex
6)
7
8func?write()?{
9????//?lock.Lock()??????????????????????????????????????????????//?加互斥鎖
10????rwlock.Lock()???????????????????????????????????????????????//?加寫鎖
11????x?=?x?+?1
12????time.Sleep(10?*?time.Millisecond)???????//?假設讀操作耗時10毫秒
13????rwlock.Unlock()?????????????????????????//?解寫鎖
14????//?lock.Unlock()??????????????????????//?解互斥鎖
15????wg.Done()
16}
17
18func?read()?{
19????//?lock.Lock()??????????????????????????????//?加互斥鎖
20????rwlock.RLock()??????????????????????????????????//?加讀鎖
21????time.Sleep(1)???????????????????????????????????????????????//?假設讀操作耗時1秒
22????rwlock.RUnlock()????????????????????????????????//?解讀鎖
23????//?lock.Unlock()????????????????????????????//?解互斥鎖
24????wg.Done()
25}
26
27func?main()?{
28????start?:=?time.Now()
29????for?i?:=?0;?i?10;?i++?{
30????????wg.Add(1)
31????????go?write()
32????}
33
34????for?i?:=?0;?i?1000;?i++?{
35????????wg.Add(1)
36????????go?read()
37????}
38
39????wg.Wait()
40????end?:=?time.Now()
41????fmt.Println(end.Sub(start))
42}
現在我們解決了多個 goroutine 同時讀寫的資源競爭問題,但是又遇到另外一個問題——性能。因為每次讀寫共享資源都要加鎖,所以性能低下,這該怎么解決呢?
現在我們分析讀寫這個特殊場景,有以下幾種情況:
寫的時候不能同時讀,因為這個時候讀取的話可能讀到臟數據(不正確的數據);
讀的時候不能同時寫,因為也可能產生不可預料的結果;
讀的時候可以同時讀,因為數據不會改變,所以不管多少個 goroutine 讀都是并發安全的。
所以就可以通過讀寫鎖 sync.RWMutex 來優化這段代碼,提升性能。
sync.Once
在實際的工作中,你可能會有這樣的需求:讓代碼只執行一次,哪怕是在高并發的情況下,比如創建一個單例。
針對這種情形,Go 語言為我們提供了 sync.Once 來保證代碼只執行一次,例如只加載一次配置文件、只關閉一次通道等。
Go語言中的sync包中提供了一個針對只執行一次場景的解決方案–sync.Once。
sync.Once只有一個Do方法,其簽名如下:
1func?(o?*Once)?Do(f?func())?{}
2//?如果要執行的函數f需要傳遞參數就需要搭配閉包來使用。
這是 Go 語言自帶的一個示例,雖然啟動了 10 個goroutine來執行 onceBody 函數,但是因為用了 once.Do 方法,所以函數 onceBody 只會被執行一次。也就是說在高并發的情況下,sync.Once 也會保證 onceBody 函數只執行一次。
sync.Once 適用于創建某個對象的單例、只加載一次的資源等只執行一次的場景。
1//?example
2func?main()?{
3???doOnce()
4}
5func?doOnce()?{
6???var?once?sync.Once
7???onceBody?:=?func()?{
8??????fmt.Println("Only?once")
9???}
10???//用于等待`goroutine`執行完畢
11???done?:=?make(chan?bool)
12???//啟動10個協程執行once.Do(onceBody)
13???for?i?:=?0;?i?10;?i++?{
14??????go?func()?{
15?????????//把要執行的函數(方法)作為參數傳給once.Do方法即可
16?????????once.Do(onceBody)
17?????????done?<-?true
18??????}()
19???}
20???for?i?:=?0;?i?10;?i++?{
21??????<-done
22???}
23}
sync.Map
1var?m?=?make(map[string]int)
2
3func?get(key?string)?int?{
4????return?m[key]
5}
6
7func?set(key?string,?value?int)?{
8????m[key]?=?value
9}
10
11func?main()?{
12????wg?:=?sync.WaitGroup{}
13????for?i?:=?0;?i?20;?i++?{
14????????wg.Add(1)
15????????go?func(n?int)?{
16????????????key?:=?strconv.Itoa(n)
17????????????set(key,?n)
18????????????fmt.Printf("k=:%v,v:=%v\n",?key,?get(key))
19????????????wg.Done()
20????????}(i)
21????}
22????wg.Wait()
23}
上面的代碼開啟少量幾個goroutine的時候可能沒什么問題,當并發多了之后執行上面的代碼就會報錯誤。
像這種場景下就需要為map加鎖來保證并發的安全性了,Go語言的sync包中提供了一個開箱即用的并發安全版map–sync.Map。開箱即用表示不用像內置的map一樣使用make函數初始化就能直接使用。同時sync.Map內置了諸如Store、Load、LoadOrStore、Delete、Range等操作方法。
一個簡單的例子
1var?m?=?sync.Map{}
2
3func?main()?{
4????wg?:=?sync.WaitGroup{}
5????for?i?:=?0;?i?20;?i++?{
6????????wg.Add(1)
7????????go?func(n?int)?{
8????????????key?:=?strconv.Itoa(n)
9????????????m.Store(key,?n)
10????????????value,?_?:=?m.Load(key)
11????????????fmt.Printf("k=:%v,v:=%v\n",?key,?value)
12????????????wg.Done()
13????????}(i)
14????}
15????wg.Wait()
16}
原子操作
代碼中的加鎖操作因為涉及內核態的上下文切換會比較耗時、代價比較高。針對基本數據類型我們還可以使用原子操作來保證并發安全,因為原子操作是Go語言提供的方法它在用戶態就可以完成,因此性能比加鎖操作更好。Go語言中原子操作由內置的標準庫sync/atomic提供。
我們填寫一個示例來比較下互斥鎖和原子操作的性能。
1package?main
2
3import?(
4????"fmt"
5????"sync"
6????"sync/atomic"
7????"time"
8)
9
10type?Counter?interface?{
11????Inc()
12????Load()?int64
13}
14
15//?普通版
16type?CommonCounter?struct?{
17????counter?int64
18}
19
20func?(c?CommonCounter)?Inc()?{
21????c.counter++
22}
23
24func?(c?CommonCounter)?Load()?int64?{
25????return?c.counter
26}
27
28//?互斥鎖版
29type?MutexCounter?struct?{
30????counter?int64
31????lock????sync.Mutex
32}
33
34func?(m?*MutexCounter)?Inc()?{
35????m.lock.Lock()
36????defer?m.lock.Unlock()
37????m.counter++
38}
39
40func?(m?*MutexCounter)?Load()?int64?{
41????m.lock.Lock()
42????defer?m.lock.Unlock()
43????return?m.counter
44}
45
46//?原子操作版
47type?AtomicCounter?struct?{
48????counter?int64
49}
50
51func?(a?*AtomicCounter)?Inc()?{
52????atomic.AddInt64(&a.counter,?1)
53}
54
55func?(a?*AtomicCounter)?Load()?int64?{
56????return?atomic.LoadInt64(&a.counter)
57}
58
59func?test(c?Counter)?{
60????var?wg?sync.WaitGroup
61????start?:=?time.Now()
62????for?i?:=?0;?i?1000;?i++?{
63????????wg.Add(1)
64????????go?func()?{
65????????????c.Inc()
66????????????wg.Done()
67????????}()
68????}
69????wg.Wait()
70????end?:=?time.Now()
71????fmt.Println(c.Load(),?end.Sub(start))
72}
73
74func?main()?{
75????c1?:=?CommonCounter{}?//?非并發安全
76????test(c1)
77????c2?:=?MutexCounter{}?//?使用互斥鎖實現并發安全
78????test(&c2)
79????c3?:=?AtomicCounter{}?//?并發安全且比互斥鎖效率更高
80????test(&c3)
81}
atomic包提供了底層的原子級內存操作,對于同步算法的實現很有用。這些函數必須謹慎地保證正確使用。除了某些特殊的底層應用,使用通道或者sync包的函數/類型實現同步更好。
sync.Cond
Cond實現了一個條件變量,它是goroutines等待或宣布事件發生的集合點。每個Cond都有一個關聯的Locker L(通常是Mutex或RWMutex),在更改條件和調用Wait方法時必須將其保留。第一次使用后,不得復制條件
在 Go 語言中,sync.WaitGroup 用于最終完成的場景,關鍵點在于一定要等待所有goroutine都執行完畢。
而 sync.Cond 可以用于發號施令,一聲令下所有goroutine都可以開始執行,關鍵點在于goroutine開始的時候是等待的,要等待 sync.Cond 喚醒才能執行。
sync.Cond 從字面意思看是條件變量,它具有阻塞協程和喚醒協程的功能,所以可以在滿足一定條件的情況下喚醒協程,但條件變量只是它的一種使用場景。
sync.Cond 有三個方法,它們分別是:
Wait,Wait原子地解鎖c.L并中止調用goroutine的執行。稍后恢復執行后,等待鎖定c.L才返回。與其他系統不同,等待不會返回,除非被廣播或信號喚醒。
Signal,信號喚醒一個等待在c的goroutin
Broadcast,喚醒所有等待c的goroutine
示例
1package?main
2
3import?(
4????"fmt"
5????"sync"
6????"time"
7)
8
9//10個人賽跑,1個裁判發號施令
10func?race()?{
11????cond?:=?sync.NewCond(&sync.Mutex{})
12????var?wg?sync.WaitGroup
13????wg.Add(11)
14????for?i?:=?0;?i?10;?i++?{
15????????go?func(num?int)?{
16????????????defer?wg.Done()
17????????????fmt.Println(num,?"號已經就位")
18????????????cond.L.Lock()
19????????????cond.Wait()?//等待發令槍響
20????????????fmt.Println(num,?"號開始跑……")
21????????????cond.L.Unlock()
22????????}(i)
23????}
24????//等待所有goroutine都進入wait狀態
25????time.Sleep(2?*?time.Second)
26????go?func()?{
27????????defer?wg.Done()
28????????fmt.Println("裁判已經就位,準備發令槍")
29????????fmt.Println("比賽開始,大家準備跑")
30????????cond.Broadcast()?//發令槍響
31????}()
32????//防止函數提前返回退出
33????wg.Wait()
34}
總結
這一節我們巴拉巴拉搞了很多,到底什么情況用哪個。相信你也可能和我一樣半懵半醒,那么我們來總結一下。他們的使用場景,啥是啥?
需知:goroutine與線程
Go語言的并發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信。
1可增長的棧
2OS線程(操作系統線程)一般都有固定的棧內存(通常為2MB),一個goroutine的棧在其生命周期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然極少會用到這么大。所以在Go語言中一次創建十萬左右的goroutine也是可以的。
3
4goroutine調度
5GPM是Go語言運行時(runtime)層面的實現,是go語言自己實現的一套調度系統。區別于操作系統調度OS線程。
6
7G很好理解,就是個goroutine的,里面除了存放本goroutine信息外?還有與所在P的綁定等信息。
8P管理著一組goroutine隊列,P里面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把占用CPU時間較長的goroutine暫停、運行后續的goroutine等等)當自己的隊列消費完了就去全局隊列里取,如果全局隊列里也消費完了會去其他P的隊列里搶任務。
9M(machine)是Go運行時(runtime)對操作系統內核線程的虛擬,?M與內核線程一般是一一映射的關系,?一個groutine最終是要放到M上執行的;
10P與M一般也是一一對應的。他們關系是:?P管理著一組G掛載在M上運行。當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G?掛載在新建的M上。當舊的G阻塞完成或者認為其已經死掉時?回收舊的M。
11
12P的個數是通過runtime.GOMAXPROCS設定(最大256),Go1.5版本之后默認為物理線程數。?在并發量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。
使用場景
Channel:關于數據流動、傳遞等情況的優先使用channle, 它是并發安全的,且性能優異, channel底層的實現為互斥鎖
sync.Once:讓代碼只執行一次,哪怕是在高并發的情況下,比如創建一個單例。
Sync.WaitGroup:用于最終完成的場景,關鍵點在于一定要等待所有協程都執行完畢。有了它我們再也不用為了等待協程執行完成而添加time.sleep了
Sync.Mutew: 當資源發現競爭時,我們可以使用Sync.Mutew,加互斥鎖保證并發安全
Sync.RWMutew:?Sync.Mutew進階使用,當讀多寫少的時候,可以使用讀寫鎖來保證并發安全,同時也提高了并發效率
sync.Map:高并發的情況下,原始的map并不安全,使用sync.Map可用讓我們的map在并發情況下也保證安全
sync.Cond:sync.Cond 可以用于發號施令,一聲令下所有goroutine都可以開始執行,關鍵點在于goroutine開始的時候是等待的,要等待 sync.Cond 喚醒才能執行。
說了這么多,這么多花里胡哨的,注意一點,Sync.Mutew,互斥鎖,所有的鎖的爸爸,原子操作。互斥鎖的叔叔。
感謝您的閱讀,如果感覺不錯。也可以、、在讀、當然推薦給身邊的哥們也是不錯的選擇,同時歡迎關注我。一起從0到1
期待下一章節,鐵索連環-context
以及下下章節:并發模式
我會在并發模式中與你探討:
channle緩存區多大比較合適,
Goroutine Work Pool,減少Goroutine過多重復的創建與銷毀
Pipeline 模式:流水線工作模式,對任務中的部分進行剖析
扇出和扇入模式:對流水線工作模式進行優化,實現更高效的扇出和扇入模式
Futures 模式:未來模式,主協程不用等待子協程返回的結果,可以先去做其他事情,等未來需要子協程結果的時候再來取
同時再一次去搞一下,到底什么是可異步、并發的代碼,并加以分析與優化
未來已來。Let‘s Go~
Go HTTP
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。