go提供了sync包和channel機制來解決協程間的同步與通訊。golang
1、sync.WaitGroup數據結構
sync包中的WaitGroup實現了一個相似任務隊列的結構,你能夠向隊列中加入任務,任務完成後就把任務從隊列中移除,若是隊列中的任務沒有所有完成,隊列就會觸發阻塞以阻止程序繼續運行,具體用法參考以下代碼:函數
package main import ( "fmt" "sync" ) var waitgroup sync.WaitGroup func Afunction(shownum int) { fmt.Println(shownum) waitgroup.Done() //任務完成,將任務隊列中的任務數量-1,其實.Done就是.Add(-1) } func main() { for i := 0; i < 10; i++ { waitgroup.Add(1) //每建立一個goroutine,就把任務隊列中任務的數量+1 go Afunction(i) } waitgroup.Wait() //.Wait()這裏會發生阻塞,直到隊列中全部的任務結束就會解除阻塞 }
咱們能夠利用sync.WaitGroup來知足這樣的狀況:spa
▲某個地方須要建立多個goroutine,而且必定要等它們都執行完畢後再繼續執行接下來的操做。.net
是的,WaitGroup最大的優勢就是.Wait()能夠阻塞到隊列中的任務都完畢後才解除阻塞。code
2、channel協程
channel是一種golang內置的類型,英語的直譯爲"通道",其實,它真的就是一根管道,並且是一個先進先出的數據結構。blog
咱們能對channel進行的操做只有4種:隊列
(1) 建立chennel (經過make()函數)同步
(2) 放入數據 (經過 channel <- data 操做)
(3) 取出數據 (經過 <-channel 操做)
(4) 關閉channel (經過close()函數)
可是channel有一些很是給力的性質須要你牢記,請必定要記住並理解好它們:
(1) channel是一種阻塞管道,是自動阻塞的。意思就是,若是管道滿了,一個對channel放入數據的操做就會阻塞,直到有某個routine從channel中取出數據,這個放入數據的操做纔會執行。相反同理,若是管道是空的,一個從channel取出數據的操做就會阻塞,直到某個routine向這個channel中放入數據,這個取出數據的操做纔會執行。這是channel最重要的一個性質,沒有之一。
package main func main() { ch := make(chan int, 3) ch <- 1 ch <- 1 ch <- 1 ch <- 1 //這一行操做就會發生阻塞,由於前三行的放入數據的操做已經把channel填滿了
package main func main() { ch := make(chan int, 3) <-ch //這一行會發生阻塞,由於channel纔剛建立,是空的,沒有東西能夠取出 }
(2)channel分爲有緩衝的channel和無緩衝的channel。兩種channel的建立方法以下:
ch := make(chan int) //無緩衝的channel,同等於make(chan int, 0) ch := make(chan int, 5) //一個緩衝區大小爲5的channel
操做一個channel時必定要注意其是否帶有緩衝,由於有些操做會觸發channel的阻塞致使死鎖。下面就來解釋這些須要注意的情景。
首先來看一個一個例子,這個例子是兩段只有主函數不一樣的代碼:
package main import "fmt" func Afuntion(ch chan int) { fmt.Println("finish") <-ch } func main() { ch := make(chan int) //無緩衝的channel go Afuntion(ch) ch <- 1 // 輸出結果: // finish }
package main import "fmt" func Afuntion(ch chan int) { fmt.Println("finish") <-ch } func main() { ch := make(chan int) //無緩衝的channel //只是把這兩行的代碼順序對調一下 ch <- 1 go Afuntion(ch) // 輸出結果: // 死鎖,無結果 }
前一段代碼最終會輸出"finish"並正常結束,可是後一段代碼會發生死鎖。爲何會出現這種現象呢,我們把上面兩段代碼的邏輯跑一下。
第一段代碼:
1. 建立了一個無緩衝channel
2. 啓動了一個goroutine,這個routine中對channel執行取出操做,可是由於這時候channel爲空,因此這個取出操做發生阻塞,可是主routine可沒有發生阻塞,它還在繼續運行呢
3. 主goroutine這時候繼續執行下一行,往channel中放入了一個數據
4. 這時阻塞的那個routine檢測到了channel中存在數據了,因此接觸阻塞,從channel中取出數據,程序就此完畢
第二段代碼:
1. 建立了一個無緩衝的channel
2. 主routine要向channel中放入一個數據,可是由於channel沒有緩衝,至關於channel一直都是滿的,因此這裏會發生阻塞。但是下面的那個goroutine尚未建立呢,主routine在這裏一阻塞,整個程序就只能這麼一直阻塞下去了,而後。。。而後就沒有而後了。。死鎖!
※從這裏能夠看出,對於無緩衝的channel,放入操做和取出操做不能再同一個routine中,並且應該是先確保有某個routine對它執行取出操做,而後才能在另外一個routine中執行放入操做。
對於帶緩衝的channel,就沒那麼多講究了,由於有緩衝空間,因此只要緩衝區不滿,放入操做就不會阻塞,一樣,只要緩衝區不空,取出操做就不會阻塞。並且,帶有緩衝的channel的放入和取出能夠用在同一個routine中。
可是,並非說有了緩衝就能夠隨意使用channel的放入和取出了,咱們必定要注意放入和取出的速率問題。下面咱們就舉個例子來講明這種問題:
咱們常常會用利用channel自動阻塞的性質來控制當前運行的goroutine的總數量,以下:
package main import ( "fmt" ) func Afunction(ch chan int) { fmt.Println("finish") <-ch //goroutine執行完了就從channel取出一個數據 } func main() { ch := make(chan int, 10) for i := 0; i < 1000; i++ { //每當建立goroutine的時候就向channel中放入一個數據,若是裏面已經有10個數據了,就會 //阻塞,由此咱們將同時運行的goroutine的總數控制在<=10個的範圍內 ch <- 1 go Afunction(ch) } // 這裏只是示範個例子,固然,接下來應該有些更加周密的同步操做 }
上面這種channel的使用方式幾乎常常會用到,可是再看一下接下來這段代碼,它和上面這種使用channel的方式幾乎同樣,可是它會形成問題:
package main func Afunction(ch chan int) { ch <- 1 ch <- 1 ch <- 1 ch <- 1 ch <- 1 <-ch } func main() { //主routine的操做同上面那段代碼 ch := make(chan int, 10) for i := 0; i < 100; i++ { ch <- 1 go Afunction(ch) } // 這段代碼運行的結果爲死鎖 }
上面這段運行和以前那一段基本上原理是同樣的,可是運行後卻會發生死鎖。爲何呢?其實總結起來就一句話,"放得太快,取得太慢了"。
按理說,咱們應該在咱們主routine中建立子goroutine並每次向channel中放入數據,而子goroutine負責從channel中取出數據。可是咱們的這段代碼在建立了子goroutine後,每一個routine會向channel中放入5個數據。這樣,每向channel中放入6個數據纔會執行一次取出操做,這樣一來就可能會有某一時刻,channel已經滿了,可是全部的routine都在執行放入操做(由於它們當前執行放入操做的機率是執行取出操做的6倍),這樣一來,全部的routine都阻塞了,從而致使死鎖。
在使用帶緩衝的channel時必定要注意放入與取出的速率問題。
(3)關閉後的channel能夠取數據,可是不能放數據。並且,channel在執行了close()後並無真的關閉,channel中的數據所有取走以後纔會真正關閉。
package main func main() { ch := make(chan int, 5) ch <- 1 ch <- 1 close(ch) ch <- 1 //不能對關閉的channel執行放入操做 // 會觸發panic }
package main func main() { ch := make(chan int, 5) ch <- 1 ch <- 1 close(ch) <-ch //只要channel還有數據,就可能執行取出操做 //正常結束 }
package main import "fmt" func main() { ch := make(chan int, 5) ch <- 1 ch <- 1 ch <- 1 ch <- 1 close(ch) //若是執行了close()就當即關閉channel的話,下面的循環就不會有任何輸出了 for { data, ok := <-ch if !ok { break } fmt.Println(data) } // 輸出: // 1 // 1 // 1 // 1 // // 調用了close()後,只有channel爲空時,channel纔會真的關閉 }
3、使用channel控制goroutine數量
channel的性質到這裏就介紹完了,可是看上去,channel的使用彷佛比WaitGroup要注意更多的細節,那麼有什麼理由必定要用channel來實現同步呢?channel相比WaitGroup有一個很大的優勢,就是channel不只能夠實現協程的同步,並且能夠控制當前正在運行的goroutine的總數。
下面就介紹幾種利用channel控制goroutine數量的方法:
1.若是任務數量是固定的:
ackage main func Afunction(ch chan int) { ch <- 1 } func main() { var ( ch chan int = make(chan int, 20) //能夠同時運行的routine數量爲20 dutycount int = 500 ) for i := 0; i < dutycount; i++ { go Afunction(ch) } //知道了任務總量,能夠像這樣利用固定循環次數的循環檢測全部的routine是否工做完畢 for i := 0; i < dutycount; i++ { <-ch } }
2.若是任務的數量不固定
package main import ( "fmt" ) func Afunction(routineControl chan int, feedback chan string) { defer func() { <-routineControl feedback <- "finish" }() // do some process // ... } func main() { var ( routineCtl chan int = make(chan int, 20) feedback chan string = make(chan string, 10000) msg string allwork int finished int ) for i := 0; i < 1000; i++ { routineCtl <- 1 allwork++ go Afunction(routineCtl, feedback) } for { msg = <-feedback if msg == "finish" { finished++ } if finished == allwork { break } } }
4、不要使用無限循環檢查goroutine是否完成工做
在使用goroutine時,咱們常常會寫出這樣的代碼:
package main import ( "fmt" ) var ( flag bool str string ) func foo() { flag = true str = "setup complete!" } func main() { go foo() for !flag { //按照咱們的本意,foo()執行完畢後,flag=true,循環就會退出。 //可是其實這個循環永遠都不會退出 } fmt.Println(str) }
運行以後發現main中的無限循環永遠也沒法退出,因此Go中不要用這種無限輪詢的方式來檢查goroutine是否完成了工做。
咱們能夠經過使用channel,讓foo()和main()實現通訊,讓foo()執行完畢後經過channel發送一個消息給main(),告訴它本身的事兒完成了,而後main()收到消息後繼續執行其餘操做:
package main import ( "fmt" ) var ( flag bool str string ) func foo(ch chan string) { flag = true str = "setup complete!" ch <- "I'm complete." //foo():個人任務完成了,發個消息給你~ } func main() { ch := make(chan string) go foo(ch) <-ch //main():OK,收到你的消息了~ for !flag { } fmt.Println(str) }
本文轉自:http://blog.csdn.net/gophers/article/details/24665419