您好,登錄后才能下訂單哦!
golang中channel是什么?怎么用?可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
并發編程是非常好的,但是并發是非常復雜的,難點在于協調,怎樣處理各個程序間的通信是非常重要的。寫channel的使用和特性之前我們需要回顧操作系統中的進程間的通信。
進程間的通信
在工程上一般通信模型有兩種:共享數據和消息。進程通信顧名思義是指進程間的信息交換,因為進程的互斥和同步就需要進程間交換信息,學過操作系統的人都知道進程通信大致上可以分為低級進程通信和高級進程通信,現在基本上都是高級進程通信。其中高級通信機制又可以分為:消息傳遞系統、共享存儲器系統、管道通信系統和客戶機服務器系統。
1、消息傳遞系統
他不借助任何共享存儲區或著某一種數據結構,他是以格式化的消息為單位利用系統提供的通信原語完成數據交換,感覺效率底下。
2、共享存儲器系統
通信的進程共享存儲區或者數據結構,進程通過這些空間進行通信,這種方式比較常見,比如某一個文件作為載體。
3、客戶機服務器系統
其他幾種通信機制基本上都是在同一個計算機上(可以說是同一環境),當然在一些情況下可以實現跨計算機通信。而客戶機-服務器系統是不一樣的,我的理解是可以當做ip請求,一個客戶機請求連接到一臺服務器。
這種方式在網絡上是現在比較流行的,現在比較常用的遠程調度,如不RPC(聽著很高大上,其實在操作系統上早就有了)還有套接字、socket,這種還是比較常用的,與我們編程緊密相關的,因為你會發現好多的服務需要使用RPC調用。
4、管道通信系
最后詳細說一下管道通信的機制,在操作系統級別管道是指用于鏈接一個讀進程和一個寫進程來實現他們之間通信的文件。系統上叫pipe文件。
實現的機制如:管道提供了下面的二個功能
1、互斥性,當一個進程正在對一個pipe文件執行讀或者寫操作時,其他的進程必須等待或阻塞或睡眠。
2、同步性,當寫(輸入)進程寫入pipe文件后會等待或者阻塞或者睡眠,直到讀(輸出)進程取走數據后把他喚醒,同理,當讀進程去讀一個空的pipe文件時也會等待或阻塞或睡眠,直到寫進程寫入pipe后把他喚醒。
channel的使用
對應到go中的channel應該是第四種,go語言的channel是在語言級別提供的goroutine間通信的方式。單獨說channel是沒有任何意義的,因為他和goroutine一起才有效果,我們先看看一般語言解決程序間共享內存的方法。
下面是一段我們熟悉的程序:
package main import "fmt" var counts int = 0 func Count() { counts++ fmt.Println(counts) } func main() { for i := 0; i < 3; i++ { go Count() } }
學過go的人都應該知道原因,因為:Go程序從初始化main() 方法和package,然后執行main()函數,但是當main()函數返回時,程序就會退出,主程序并不等待其他goroutine的,導致沒有任何輸出。
我們看看常規語言是怎樣解決這種并發的問題的:
package main import "fmt" import "sync" import "runtime" var counts int = 0 func Count(lock *sync.Mutex) { lock.Lock() counts++ fmt.Println(counts) lock.Unlock() } func main() { lock := &sync.Mutex{} for i := 0; i < 3; i++ { go Count(lock) } for { lock.Lock() c := counts lock.Unlock() runtime.Gosched() if c >= 3 { break } } }
解決方式有點逗比,加了一堆的鎖,因為他的執行是這樣的:代碼中的lock變量,每次對counts的操作,都要先將他鎖住,操作完成后,再將鎖打開,在主函數中,使用for循環來不斷檢查counter的值當然同樣也要加鎖。
當其值達到3時,說明所有goroutine都執行完畢了,這時主函數返回,然后程序退出。這種方式是大眾語言解決并發的首選方式,可以看到為了解決并發,多寫了好多的東西,如果一個初具規模的項目,不知道要加多少鎖。
我們看看channel是如何解決這種問題的:
package main import "fmt" var counts int = 0 func Count(i int, ch chan int) { fmt.Println(i, "WriteStart") ch <- 1 fmt.Println(i, "WriteEnd") fmt.Println(i, "end", "and echo", i) counts++ } func main() { chs := make([]chan int, 3) for i := 0; i < 3; i++ { chs[i] = make(chan int) fmt.Println(i, "ForStart") go Count(i, chs[i]) fmt.Println(i, "ForEnd") } fmt.Println("Start debug") for num, ch := range chs { fmt.Println(num, "ReadStart") <-ch fmt.Println(num, "ReadEnd") } fmt.Println("End") //為了使每一步數值全部打印 for { if counts == 3 { break } } }
為了看清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的基本語法比較簡單, 一般的聲明格式是:
var ch chan ElementType
定義格式如下:
ch := make(chan int)
還有一個最常用的就是寫入和讀出,當你向channel寫入數據時會導致程序阻塞,直到有其他goroutine從這個channel中讀取數據,同理如果channel之前沒有寫入過數據,那么從channel中讀取數據也會導致程序阻塞,直到這個channel中被寫入了數據為止
ch <- value //寫入 value := <-ch //讀取
關閉channel
close(ch)
判斷channel是否關閉(利用多返回值的方式):
b, status := <-ch
帶緩沖的channel,說起來也容易,之前我們使用的都是不帶緩沖的channel,這種方法適用于單個數據的情況,對于大量的數據不太實用,在調用make()的時候將緩沖區大小作為第二個參數傳入就可以創建緩沖的channel,即使沒有讀取方,寫入方也可以一直往channel里寫入,在緩沖區被填完之前都不會阻塞。
c := make(chan int, 1024)
單項channel,單向channel只能用于寫入或者讀取數據。channel本身必然是同時支持讀寫的,否則根本沒法用。所謂的單向channel概念,其實只是對channel的一種使用限制。單向channel變量的聲明:
var ch2 chan int // ch2是一個正常的channel var ch3 <-chan int // ch3是單向channel,只用于讀取int數據
單項channel的初始化
ch4 := make(chan int) ch5 := <-chan int(ch4) // ch5是一個單向的讀取channel
超時機制
超時機制其實也是channel的錯誤處理,channel固然好用,但是有時難免會出現實用錯誤,當是讀取channel的時候發現channel為空,如果沒有錯誤處理,像這種情況就會使整個goroutine鎖死了,無法運行。
我找了好多資料和說法,channel 并沒有處理超時的方法,但是可以利用其它方法間接的處理這個問題,可以使用select機制處理,select的特點比較明顯,只要有一個case完成了程序就會往下運行,利用這種方法,可以實現channel的超時處理:
原理如下:我們可以先定義一個channel,在一個方法中對這個channel進行寫入操作,但是這個寫入操作比較特殊,比如我們控制5s之后寫入到這個channel中,這5s時間就是其他channel的超時時間,這樣的話5s以后如果還有channel在執行,可以判斷為超時,這是channel寫入了內容,select檢測到有內容就會執行這個case,然后程序就會順利往下走了。
實現如下:
timeout := make(chan bool, 1) go func() { time.Sleep(5s) // 等待s秒鐘 timeout <- true }() select { case <-ch: // 從ch中讀取到數據 case <-timeout: // 沒有從ch中讀取到數據,但從timeout中讀取到了數據 }
看完上述內容,你們對golang中channel有進一步的了解嗎?如果還想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。