GO語言之channel

前言:
  初識go語言不到半年,我是一次偶然的機會認識了golang這門語言,看到他簡潔的語法風格和強大的語言特性,瞬間有了學習他的興趣。我是很看好go這樣的語言的,一方面由於他有谷歌主推,另外一方面他確實有用武之地,高併發就是他的長處。如今的國內徹底使用go開發的項目還不是不少,從這個上面能夠看到:連接https://github.com/qiniu/go/issues/15,據我瞭解七牛雲存儲應該是第一個徹底使用go開發的大型項目,其中七牛雲的CEO許世偉是公認的go專家,同時也是《go語言編程》的做者,另外美團、小米、360、新浪等公司或多或少都有go語言的使用。php

  在我看來go是一門值得去學習去學習的語言。我原本是學習php的,有人會第一時間反駁我,php學習的咋樣啊,就慌着去學習其餘語言,我想說的是這不衝突,做爲一個後端開發者,只會php一門腳本式的弱類型語言是遠遠不夠的,這裏不是說php語言很差。php有php的好,編譯語言,強類型語言也自有他的優點所在,而服務器端開發者須要在併發,多線程上有所涉獵,總不能5年8年以後還寫php吧,你要知道好多的架構師是沒有語言的限制的。我就是一個不安分的人,不喜歡循序漸進的生活,趁如今還年輕,喜歡啥就會全力去學習,好了,扯淡的話就說這麼多。html

  這篇博客寫的是go語言中的channel,之因此寫他是由於我感受channel很重要,同時channel也是go併發的重要支撐點,由於go是使用消息傳遞共享內存而不是使用共享內存來通訊。併發編程是很是好的,可是併發是很是複雜的,難點在於協調,怎樣處理各個程序間的通訊是很是重要的。寫channel的使用和特性以前咱們須要回顧操做系統中的進程間的通訊。git

進程間的通訊github

  在工程上通常通訊模型有兩種:共享數據和消息。進程通訊顧名思義是指進程間的信息交換,由於進程的互斥和同步就須要進程間交換信息,學過操做系統的人都知道進程通訊大體上能夠分爲低級進程通訊和高級進程通訊,如今基本上都是高級進程通訊。其中高級通訊機制又能夠分爲:消息傳遞系統、共享存儲器系統、管道通訊系統和客戶機服務器系統。

  一、消息傳遞系統
  他不借助任何共享存儲區或着某一種數據結構,他是以格式化的消息爲單位利用系統提供的通訊原語完成數據交換,感受效率底下。

  二、共享存儲器系統
  通訊的進程共享存儲區或者數據結構,進程經過這些空間進行通訊,這種方式比較常見,好比某一個文件做爲載體。

  三、客戶機服務器系統
  其餘幾種通訊機制基本上都是在同一個計算機上(能夠說是同一環境),固然在一些狀況下能夠實現跨計算機通訊。而客戶機-服務器系統是不同的,個人理解是能夠當作ip請求,一個客戶機請求鏈接到一臺服務器。這種方式在網絡上是如今比較流行的,如今比較經常使用的遠程調度,如不RPC(聽着很高大上,其實在操做系統上早就有了)還有套接字、socket,這種仍是比較經常使用的,與咱們編程緊密相關的,由於你會發現好多的服務須要使用RPC調用。

  四、管道通訊系
  最後詳細說一下管道通訊的機制,在操做系統級別管道是指用於連接一個讀進程和一個寫進程來實現他們之間通訊的文件。系統上叫pipe文件。實現的機制如:管道提供了下面的二個功能,一、互斥性,當一個進程正在對一個pipe文件執行讀或者寫操做時,其餘的進程必須等待或阻塞或睡眠。二、同步性,當寫(輸入)進程寫入pipe文件後會等待或者阻塞或者睡眠,直到讀(輸出)進程取走數據後把他喚醒,同理,當讀進程去讀一個空的pipe文件時也會等待或阻塞或睡眠,直到寫進程寫入pipe後把他喚醒。golang

channel的使用
  好了,上面花了很多的篇幅寫了進程間通訊的幾種方式,咱們再回過來看看channel,對應到go中的channel應該是第四種,go語言的channel是在語言級別提供的goroutine間通訊的方式。單獨說channel是沒有任何意義的,由於他和goroutine一塊兒纔有效果,咱們先看看通常語言解決程序間共享內存的方法,下面是一段咱們熟悉的程序,什麼也不會輸出,我剛學習的時候認爲會輸出東西,可是實際不是這樣,當是感到一臉懵逼。編程

 1 package main
 2 
 3 import "fmt"
 4 
 5 var counts int = 0
 6 
 7 func Count() {
 8     counts++
 9     fmt.Println(counts)
10 }
11 func main() {
12 
13     for i := 0; i < 3; i++ {
14         go Count()
15     }
16 }

  學過go的人都應該知道緣由,由於:Go程序從初始化main() 方法和package,而後執行main()函數,可是當main()函數返回時,程序就會退出,主程序並不等待其餘goroutine的,致使沒有任何輸出。咱們看看常規語言是怎樣解決這種併發的問題的:後端

 1 package main
 2 
 3 import "fmt"
 4 import "sync"
 5 import "runtime"
 6 
 7 var counts int = 0
 8 
 9 func Count(lock *sync.Mutex) {
10     lock.Lock()
11     counts++
12     fmt.Println(counts)
13     lock.Unlock()
14 }
15 func main() {
16     lock := &sync.Mutex{}
17 
18     for i := 0; i < 3; i++ {
19         go Count(lock)
20     }
21 
22     for {
23         lock.Lock()
24         c := counts
25         lock.Unlock()
26 
27         runtime.Gosched()
28 
29         if c >= 3 {
30             break
31         }
32 
33     }
34 }

  解決方式有點逗比,加了一堆的鎖,由於他的執行是這樣的:代碼中的lock變量,每次對counts的操做,都要先將他鎖住,操做完成後,再將鎖打開,在主函數中,使用for循環來不斷檢查counter的值固然一樣也要加鎖。當其值達到3時,說明全部goroutine都執行完畢了,這時主函數返回,而後程序退出。這種方式是大衆語言解決併發的首選方式,能夠看到爲了解決併發,多寫了好多的東西,若是一個初具規模的項目,不知道要加多少鎖。七牛雲存儲

  咱們看看channel是如何解決這種問題的:服務器

 1 package main
 2 
 3 import "fmt"
 4 
 5 var counts int = 0
 6 
 7 func Count(i int, ch chan int) {
 8     fmt.Println(i, "WriteStart")
 9     ch <- 1
10     fmt.Println(i, "WriteEnd")
11     fmt.Println(i, "end", "and echo", i)
12     counts++
13 }
14 
15 func main() {
16     chs := make([]chan int, 3)
17     for i := 0; i < 3; i++ {
18         chs[i] = make(chan int)
19         fmt.Println(i, "ForStart")
20         go Count(i, chs[i])
21         fmt.Println(i, "ForEnd")
22     }
23 
24     fmt.Println("Start debug")
25     for num, ch := range chs {
26         fmt.Println(num, "ReadStart")
27         <-ch
28         fmt.Println(num, "ReadEnd")
29     }
30 
31     fmt.Println("End")
32 
33     //爲了使每一步數值所有打印
34     for {
35         if counts == 3 {
36             break
37         }
38     }
39 }

爲了看清goroutine執行的步驟和channel的特性,我特地在每一步都作了打印,下面是執行的結果,感興趣的同窗能夠本身試試,打印的順序可能不同:網絡

  下面咱們分析一下這個流程,看看channel在裏面的做用。主程序開始:

  打印 "0 ForStart 0 ForEnd" ,表示 i = 0 這個循環已經開始執行了,第一個goroutine已經開始;

  打印 "1 ForStart"、"1 ForEnd"、"2 ForStart"、"2 ForEnd" 說明3次循環都開始,如今系統中存在3個goroutine;

  打印 "Start debug",說明主程序繼續往下走了,

  打印 "0 ReadStar"t ,說明主程序執行到for循環,開始遍歷chs,一開始遍歷第一個,可是由於此時 i = 0 的channel爲空,因此該channel的Read操做阻塞;

  打印 "2 WriteStart",說明第一個 i = 2 的goroutine先執行到Count方法,準備寫入channel,由於主程序讀取 i = 0 的channel的操做再阻塞中,因此 i = 2的channel的讀取操做沒有執行,如今i = 2 的goroutine 寫入channel後下面的操做阻塞;

  打印 "0 WriteEnd",說明 i = 0 的goroutine也執行到Count方法,準備寫入channel,此時主程序 i = 0 的channel的讀取操做被喚醒;

  打印 "0 WriteEnd" 和 "0 end and echo 0" 說明寫入成功;

  打印 "0 ReadEnd",說明喚醒的 i = 0 的channel的讀取操做已經喚醒,而且讀取了這個channel的數據;

  打印 "0 ReadEnd",說明這個讀取操做結束;

  打印 "1 ReadStart",說明 i = 1 的channel讀取操做開始,由於i = 1 的channel沒有內容,這個讀取操做只能阻塞;

  打印 "1 WriteStart",說明 i = 1 的goroutine 執行到Count方法,開始寫入channel 此時 i = 1的channel讀取操做被喚醒;

  打印 "1 WriteEnd" 和 "1 end and echo 1" 說明 i = 1 的channel寫入操做完成;

  打印 "1 ReadEnd",說明 i = 1 的讀取操做完成;

  打印 "2 ReadStart",說明 i = 2 的channel的讀取操做開始,由於以前已經執行到 i = 2 的goroutine寫入channel操做,只是阻塞了,如今由於讀取操做的進行,i = 2的寫入操做流程繼續執行;

  打印 "2 ReadEnd",說明 i = 2 的channel讀取操做完成;

  打印 "End" 說明主程序結束。

  此時可能你會有疑問,i = 2 的goroutine尚未結束,主程序爲啥就結束了,這正好印證了咱們開始的時候說的,主程序是不等待非主程序完成的,因此按照正常的流程咱們看不到 i = 2 的goroutine的的徹底結束,這裏爲了看到他的結束我特地加了一個 counts 計算器,只有等到計算器等於3的時候才結束主程序,接着就出現了打印 "2 WriteEnd" 和 "2 end and echo 2"  到此全部的程序結束,這就是goroutine在channel做用下的執行流程。

  上面分析寫的的比較詳細,耐心看兩遍基本上就明白了,主要幫助你們理解channel的寫入阻塞和讀入阻塞的應用。

 

基本語法

channel的基本語法比較簡單, 通常的聲明格式是:

1 var ch chan ElementType

定義格式以下:

1 ch := make(chan int)

還有一個最經常使用的就是寫入和讀出,當你向channel寫入數據時會致使程序阻塞,直到有其餘goroutine從這個channel中讀取數據,同理若是channel以前沒有寫入過數據,那麼從channel中讀取數據也會致使程序阻塞,直到這個channel中被寫入了數據爲止

1 ch <- value    //寫入
2 value := <-ch  //讀取

關閉channel

close(ch)

判斷channel是否關閉(利用多返回值的方式):

1 b, status := <-ch

帶緩衝的channel,提及來也容易,以前咱們使用的都是不帶緩衝的channel,這種方法適用於單個數據的狀況,對於大量的數據不太實用,在調用make()的時候將緩衝區大小做爲第二個參數傳入就能夠建立緩衝的channel,即便沒有讀取方,寫入方也能夠一直往channel裏寫入,在緩衝區被填完以前都不會阻塞。

c := make(chan int, 1024)

單項channel,單向channel只能用於寫入或者讀取數據。channel自己必然是同時支持讀寫的,不然根本無法用。所謂的單向channel概念,其實只是對channel的一種使用限制。單向channel變量的聲明:

1 var ch1 chan int   // ch1是一個正常的channel
2 var ch2 <-chan int // ch2是單向channel,只用於讀取int數據

單項channel的初始化

1 ch3 := make(chan int)
2 ch4 := <-chan int(ch3) // ch4是一個單向的讀取channel

 

超時機制

  超時機制其實也是channel的錯誤處理,channel當然好用,可是有時不免會出現實用錯誤,當是讀取channel的時候發現channel爲空,若是沒有錯誤處理,像這種狀況就會使整個goroutine鎖死了,沒法運行,我找了好多資料和說法,channel 並無處理超時的方法,可是能夠利用其它方法間接的處理這個問題,可使用select機制處理,select的特色比較明顯,只要有一個case完成了程序就會往下運行,利用這種方法,能夠實現channel的超時處理:

  原理以下:咱們能夠先定義一個channel,在一個方法中對這個channel進行寫入操做,可是這個寫入操做比較特殊,好比咱們控制5s以後寫入到這個channel中,這5s時間就是其餘channel的超時時間,這樣的話5s之後若是還有channel在執行,能夠判斷爲超時,這是channel寫入了內容,select檢測到有內容就會執行這個case,而後程序就會順利往下走了。實現以下:

 1 timeout := make(chan bool, 1)
 2 go func() {
 3     time.Sleep(5s) // 等待s秒鐘
 4     timeout <- true
 5 }()
 6 
 7 select {
 8     case <-ch:
 9     // 從ch中讀取到數據
10     case <-timeout:
11     // 沒有從ch中讀取到數據,但從timeout中讀取到了數據
12 }

好了,今天就寫這麼多,寫了一上午了,該吃飯了。

初學go語言,沒有作過系統的項目,只是比較感興趣,但願之後深刻學習這門語言,文章中不對之處或者是理解上的誤差請大神在評論處指出來,你們共同窗習。

注意:
一、本博客同步更新到個人我的網站:http://www.zhaoyafei.cn
二、本文屬原創內容,爲了尊重他人勞動,轉載請註明本文地址:
相關文章
相關標籤/搜索