使用goroutine的方法很簡單,直接在語句前面加go關鍵字即可,如果是多核處理器的電腦,使用gorountine,就會在另外一個CPU上執行goroutine,子協程不一定會和主協程在一個CPU上執行。
這裡有兩個注意的地方,使用go關鍵字的進程稱之為子協程,而沒有使用go關鍵字的進程稱之為主協程,在多CPU的機器上,如果有多個協程,那麼這些協程的執行順序以及執行完成的順序都是不確定的,但有一點,如果主協程結束,那麼整個進程就結束了,不論子協程是否結束,整個進程都結束了,也就看不到子協程的運行結果了。
樣本1
package mainimport "fmt"func main() {go fmt.Println("hello") //子協程fmt.Println("world") //主協程}
上面的代碼執行之後,會有三種結果:
1、hello world 2、world 3、world hello
上面這三個結果都是正確的。
對於第一種,子協程在一個CPU上面執行輸出hello之後,再輪到主協程中執行輸出world。
對於第二種,子協程在一個CPU上還沒來得及執行,或者說沒有執行完成,但是主協程已經執行完成了,此時整個進程就運行完畢了,所以就看不到子協程的運行結果了。
對於第三種,主協程中語句還沒執行完成的時候,子協程已經運行完畢,幾乎同時完成,所以會出現著這種情況。
可以參考下面這個圖來理解:
建立channel
channel的零值是nil
chi := make(chan int)chs := make(chan string)chif := make(chan interface{}) //因為可以認為所有的類型都實現了空介面//所以這個chif的chan接收任何類型的chan
發送和接收channel
chi := make(chan int)chi <- 3//發送i := <-chi//接收,儲存值<-chi//接收,不儲存值
關閉channel
chi := make(chan int)chi <- 3 //發送i := <-chi //接收,儲存值<-chi //接收,不儲存值close(chi) //關閉channel
對一個已經關閉的channel發送資料,都會導致panic異常。對於一個已經關閉的channel,可以繼續接收這個已關閉的channel中的資料,如果沒有資料的話,接收到的就是chan類型的零值。
無緩衝channel(同步channel)
一個基於無緩衝的channel:
1、發送操作將導致寄件者的gorountine阻塞,直到有兩外一個goroutine在相同的channel上執行接收操作之後,寄件者的阻塞才會解開。
2、同理,如果接收操作先發生,那麼接受者goroutine也將阻塞,直到另一個goroutine在相同的channel上執行發送操作,接受者的阻塞才會解開。
基於無緩衝的channel的發送和接收操作將導致兩個goroutine做一次同步操作,所以也叫作同步channel。
向一個無緩衝的channel中發送多次資料,即多個goroutine都向同一個goroutine中發送資料,並且只有一個協程來接收資料,這個時候會後什麼問題嗎?會發生覆蓋?或者是只有第一個發送的值才會保留,後面發送的資料都無效?和有緩衝channel有什麼區別?
請看下面的樣本:
package mainimport "fmt"func main() {chi := make(chan int) //無緩衝go func() {chi <- 10chi <- 30chi <- 40}()//fmt.Println(<-chi)//10//fmt.Println(<-chi, <-chi) //10 30//fmt.Println(<-chi, <-chi, <-chi)//10 30 40fmt.Println(<-chi, <-chi, <-chi, <-chi) //deadlock}
從上面的例子中可以看到,有一個gorountine向一個無緩衝的channel中發送三個資料,此時,分這幾種情況:
0、chi還沒有來得及向channel中發送資料時,主協程阻塞,子協程可以向其中發送一個資料。
1、 主協程只從channel中接收一次資料:子協程向chi中發送了一個資料,此時,自協程阻塞,等待chi中的資料被消費之後,才繼續運行。 那麼,主協程接收的第一個資料就是第一次向這個channel中發送的資料10,之後,channel空閑,寄件者可以發送第二個資料30,子協程再次阻塞,等待主協程接收資料,但是此時主協程不接受資料,主協程已經執行完成了,那麼整個進程就完成了,所以最終只輸出10這一個資料。此時子協程也會跟著被結束。
2、 主協程只從channel中接收兩次資料:接著上一步,此時主協程第二次接收資料的話,接收到的是30;之後,chi空閑,子協程再次向chi中發送資料40,然後又阻塞,但是此時主協程已經完成,所以子協程再次被結束。
3、主協程從channel中接收三次資料:接著上一步,此時主協程接收到的資料應該是40,之後,chi空閑,子協程也不會像其中發送資料了,同時,子協程也已經運行結束了,所以,主協程中輸出10 30 40。
4、主協程從channel中接收四次資料:接著上一步,因為chi已經空閑,且子協程已經結束,表示沒有協程向chi中發送資料了,但是chi並沒有關閉,那麼主協程中第四次的接收資料就會一直等待,最終造成死結。解決這個問題,可以在子協程中發送第三次資料40之後,執行close(chi),之後,主協程第四次接收資料便不會引起死結,接收到的資料時0(chi的類型零值)
使用range遍曆channel
package mainimport ("fmt")func main() {num := make(chan int)go func() {num <- 30num <- 40close(num)}()for x := range num {fmt.Println(x)}//輸出//30//40}
使用range遍曆channel時,從channel中接收資料,如果從channel中擷取到了資料,那麼返回的結果就是擷取到的值;如果channel已經關閉,那麼對於無緩衝的channel而言,就沒有資料可擷取了,但是range的結果是channel的類型零值。
串聯channel
下面是一個串聯的channel,實現
package mainimport ("fmt""time")func main() {num := make(chan int)sqrt := make(chan int)go func() {for x := 0; ; x++ {num <- xtime.Sleep(time.Second)}}()go func() {for {x := <-numsqrt <- x * x}}()for {fmt.Println(<-sqrt)}}
上面這個代碼中,存在問題:如果第一個子協程不再往num中發送資料,那麼,第二個子協程就會阻塞,同理,主協程sqrt取資料也會阻塞,最終引發死結。
解決方案:
package mainimport ("fmt""time")func main() {num := make(chan int)sqrt := make(chan int)go func() {for x := 0; x < 10; x++ {num <- xtime.Sleep(time.Second)}//不再向num中發送資料時,關閉channel,防止死結close(num)}()go func() {for x := range num {sqrt <- x * x}//num已經關閉了,所以也要關閉sqrtclose(sqrt)}()for x := range sqrt {fmt.Println(x)}}
單向channel
使用channel作為參數進行通訊時,在函數內部,有的channel只接受資料,有的channel只發送資料,這是可以使用單向的channel表示。
雙向的channel:因為關閉channel操作 一般是 發送方通知接收方,發送方不再向channel中發送新的資料了,所以通常只有在寄件者所在的goroutine才會調用close函數,在接收channel資料的goroutine中關閉channel顯然是不合理的,很有可能會引起錯誤,因為發送資料的goroutine可能不知道channel已經關閉了,所以他一旦向channle中發送資料,就會引發錯誤。
單向channel:只有寄件者的goroutine中可以調用close函數來關閉單項channel。如果一個goroutine中,對一個只接受的channel調用close函數,或引發編譯錯誤。
任何雙向的channel向單向的channel賦值都將導致隱式轉換,但是不能反向轉換。
聲明單向channle
chi := make(<-chan int) //只能被讀資料的channelchi := make(chan <- int) //只接收資料的channel
有緩衝的channel
chi := make(chan int)//無緩衝chii := make(chan int,10)//有緩衝channel,緩衝空間可存10個值
定義了一個有緩衝的channel之後:
在往channel中發資料的時候,只有當channel的緩衝區數量不為0時,資料才能寫入,寫資料的goroutine才不會阻塞。
在從channel中讀資料的時候,只有當channel不為空白的時候,讀channel的goroutine才不會阻塞。
package mainimport ("fmt""time")func WriteData(chi chan int) {go func() {for x := 0; ; x++ {chi <- xtime.Sleep(2 * time.Second)}}()go func() {for x := 0; ; x++ {chi <- xtime.Sleep(time.Second)}}()go func() {for x := 0; ; x++ {chi <- xtime.Sleep(2 * time.Second)}}()}func main() {chi := make(chan int, 3)WriteData(chi)for x := range chi {fmt.Println(x)}}
此例中,如果寫資料時channel已滿,那麼寫資料的goroutine會阻塞。如果讀channel時,channel為空白,那麼讀資料的goroutine會阻塞。
並發中的迴圈
package mainimport "fmt"func loop() {for x := 0; x < 10; x++ {go func() {fmt.Println(x)}()}}func main() {loop()}
上面這個代碼執行的時候,可能有以下問題:
1、沒有輸出 2、只輸出一部分值 3、輸出很多相同的值 4、輸出的值全相同
對於1、2、3的問題,是因為主協程執行速度太快,各個goroutine還沒來得及執行,主協程就結束了,於是整個進程就結束了,也就看不到輸出了。
解決方案:在主協程中加一個休眠,給各個協程時間去執行。
package mainimport ("fmt""time")func loop() {for x := 0; x < 10; x++ {go func() {fmt.Println(x)}()}}func main() {loop()time.Sleep(time.Second)}
對於第四個問題,是因為loop中的迴圈太快,而各個goroutine太慢,於是所有goroutine在執行前,迴圈已經完畢,此時i為10,於是全部都列印10了。
解決方案:將i作為參數(閉包)
package mainimport ("fmt""time")func loop() {for x := 0; x < 10; x++ {go func(i int) {//i為形參fmt.Println(i)}(x) //x為實參}}func main() {loop()time.Sleep(time.Second)}
select接收channel
package mainimport ("fmt")func loop(ch chan int, chi chan int) {go func() {for {ch <- 9}}()go func() {for {chi <- 8}}()}func main() {ch, chi := make(chan int), make(chan int)loop(ch, chi)select {case <-ch:fmt.Println("接收到ch中的資料")case <-chi:fmt.Println("接收到chi中的資料")}}
select類似於if else,每一個case表示接收到channel的資料,執行對應的操作。只不過上面這個例子中,只會接收一次,要麼接收的是ch,要麼是chi,之後就不會再從channel中讀資料了。
如果要一直等待接收多個channel,即,要接收多個channel中的資料,可以使用for + select即可:
package mainimport ("fmt""time")func loop(ch chan int, chi chan int) {go func() {for {ch <- 9time.Sleep(2 * time.Second)}}()go func() {for {chi <- 8time.Sleep(3 * time.Second)}}()}func main() {ch, chi := make(chan int), make(chan int)loop(ch, chi)for {select {case <-ch:fmt.Println("接收到ch中的資料")case <-chi:fmt.Println("接收到chi中的資料")}}}
並發的退出
package mainimport ("fmt""time")func main() {exit := make(chan bool)for x := 1; x < 5; x++ {go func(i int) {for {select {case <-exit:fmt.Println("第", i, "個goroutine結束運行")returndefault:fmt.Println("第", i, "個goroutine正在運行")}time.Sleep(time.Second)}}(x)}time.Sleep(time.Second * 5) //各個goroutine運行close(exit) //廣播結束訊息time.Sleep(time.Second * 1) //等待各個goroutine列印結束資訊}