GO語言之channel


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

  在我看來go是一門值得去學習去學習的語言。我本來是學習php的,有人會第一時間反駁我,php學習的咋樣啊,就慌着去學習其他語言,我想說的是這不沖突,作為一個后端開發者,只會php一門腳本式的弱類型語言是遠遠不夠的,這里不是說php語言不好。php有php的好,編譯語言,強類型語言也自有他的優勢所在,而服務器端開發者需要在並發,多線程上有所涉獵,總不能5年8年之后還寫php吧,你要知道好多的架構師是沒有語言的限制的。我就是一個不安分的人,不喜歡按部就班的生活,趁現在還年輕,喜歡啥就會全力去學習,好了,扯淡的話就說這么多。

  這篇博客寫的是go語言中的channel,之所以寫他是因為我感覺channel很重要,同時channel也是go並發的重要支撐點,因為go是使用消息傳遞共享內存而不是使用共享內存來通信。並發編程是非常好的,但是並發是非常復雜的,難點在於協調,怎樣處理各個程序間的通信是非常重要的。寫channel的使用和特性之前我們需要回顧操作系統中的進程間的通信。

進程間的通信

  在工程上一般通信模型有兩種:共享數據和消息。進程通信顧名思義是指進程間的信息交換,因為進程的互斥和同步就需要進程間交換信息,學過操作系統的人都知道進程通信大致上可以分為低級進程通信和高級進程通信,現在基本上都是高級進程通信。其中高級通信機制又可以分為:消息傳遞系統、共享存儲器系統、管道通信系統和客戶機服務器系統。

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

  2、共享存儲器系統
  通信的進程共享存儲區或者數據結構,進程通過這些空間進行通信,這種方式比較常見,比如某一個文件作為載體。

  3、客戶機服務器系統
  其他幾種通信機制基本上都是在同一個計算機上(可以說是同一環境),當然在一些情況下可以實現跨計算機通信。而客戶機-服務器系統是不一樣的,我的理解是可以當做ip請求,一個客戶機請求連接到一台服務器。這種方式在網絡上是現在比較流行的,現在比較常用的遠程調度,如不RPC(聽着很高大上,其實在操作系統上早就有了)還有套接字、socket,這種還是比較常用的,與我們編程緊密相關的,因為你會發現好多的服務需要使用RPC調用。

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

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語言,沒有做過系統的項目,只是比較感興趣,希望以后深入學習這門語言,文章中不對之處或者是理解上的偏差請大神在評論處指出來,大家共同學習。

注意:
1、本博客同步更新到我的個人網站:http://www.zhaoyafei.cn
2、本文屬原創內容,為了尊重他人勞動,轉載請注明本文地址:
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM