本文寫的是go語言中的channel,之所以寫他是因為我感覺channel很重要,同時channel也是go並發的重要支撐點,因為go是使用訊息傳遞共享記憶體而不是使用共享記憶體來通訊。
並發程式設計是非常好的,但是並發是非常複雜的,困難在於協調,怎樣處理各個程式間的通訊是非常重要的。寫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 ch1 chan int // ch1是一个正常的channel var ch2 <-chan int // ch2是单向channel,只用于读取int数据
单项channel的初始化
ch3 := make(chan int) 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,然后程序就会顺利往下走了。
实现如下:
timeout := make(chan bool, 1) go func() { time.Sleep(5s) // 等待s秒钟 timeout <- true }() select { case <-ch: // 从ch中读取到数据 case <-timeout: // 没有从ch中读取到数据,但从timeout中读取到了数据 }
推荐:go语言教程
以上是go語言中channel的詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!