CSP
要想理解 channel 要先知道 CSP 模型郊愧。CSP 是 Communicating Sequential Process 的簡稱娜搂,中文可以叫做通信順序進(jìn)程漠秋,是一種并發(fā)編程模型吻谋,由 Tony Hoare 于 1977 年提出辽幌。簡單來說增淹,CSP 模型由并發(fā)執(zhí)行的實體(線程或者進(jìn)程)所組成,實體之間通過發(fā)送消息進(jìn)行通信乌企,這里發(fā)送消息時使用的就是通道,或者叫 channel成玫。CSP 模型的關(guān)鍵是關(guān)注 channel加酵,而不關(guān)注發(fā)送消息的實體。Go 語言實現(xiàn)了 CSP 部分理論哭当,goroutine 對應(yīng) CSP 中并發(fā)執(zhí)行的實體猪腕,channel 也就對應(yīng)著 CSP 中的 channel。
channel
channel是Go語言在語言級別提供的goroutine間的通信方式钦勘。我們可以使用channel在兩個或多個goroutine之間傳遞消息陋葡。在聲明一個通道變量的時候,必須確定通道類型的傳遞的元素類型彻采,通過channel傳遞對象的過程和調(diào)用函數(shù)時的參數(shù)傳遞類型必須一致腐缤。
channel的聲明形式為:
var 變量名稱 chan 通道元素類型
與一般的變量聲明不同的地方僅僅是在類型之前加了chan關(guān)鍵字,初始化一個channel也很簡單,直接使用內(nèi)置的函數(shù)make()即可:
make(chan Type) //等價于make(chan Type, 0)
make(chan Type, capacity)
make函數(shù)可接受兩個參數(shù)肛响。第一個參數(shù)是代表了將被初始化的值的類型的字面量(比如chan int)岭粤,而第二個參數(shù)則是通道的容量,是可選參數(shù)特笋,例如剃浇,若我們想要初始化一個長度為3且元素類型為int的通道值,則需要這樣寫:
make(chan int, 3)
確切地說猎物,通道值的長度應(yīng)該被稱為其緩存的尺寸虎囚。換句話說,它代表著通道值中可以暫存的數(shù)據(jù)的個數(shù)蔫磨。因此通道容量不能是負(fù)數(shù)淘讥,一個通道類似于一個先進(jìn)先出(FIFO)的隊列,即:越早被放入(或稱發(fā)送)到通道值的數(shù)據(jù)會越先被取出(或稱接收)在channel的用法中质帅。
chan數(shù)據(jù)發(fā)送與接收
chan數(shù)據(jù)發(fā)送與接收都是用到左尖括號與減號組合(<-),一個左尖括號緊接著一個減號的組合形似一個箭頭适揉,箭頭的方向代表了元素值的傳輸方向留攒。
** 發(fā)送數(shù)據(jù)<-**
向channel發(fā)送(寫入)數(shù)據(jù)通常會導(dǎo)致程序阻塞,直到有其他goroutine從這個channel中讀取數(shù)據(jù)嫉嘀。下面的程序會出現(xiàn)死鎖:
func foo(in chan int) {
fmt.Println(<-in)
}
func main() {
out := make(chan int)
out <- 1
go foo(out)
}
讀取數(shù)據(jù)
value := <- ch
value, ok := <-ch //功能同上炼邀,同時檢查通道是否已關(guān)閉或者是否為空
因此需要特別注意的是:channel接收和發(fā)送數(shù)據(jù)都是阻塞的,當(dāng)把數(shù)據(jù)發(fā)送到信道時剪侮,程序控制會在發(fā)送數(shù)據(jù)的語句處發(fā)生阻塞拭宁,直到有其它 Go 協(xié)程從信道讀取到數(shù)據(jù),才會解除阻塞瓣俯。與此類似杰标,當(dāng)讀取信道的數(shù)據(jù)時,如果沒有其它的協(xié)程把數(shù)據(jù)寫入到這個信道彩匕,那么讀取過程就會一直阻塞著腔剂。
import "fmt"
func main() {
c := make(chan int)
go func(){
defer fmt.Println("子協(xié)程結(jié)束")
fmt.Println("子協(xié)程正在運行")
c<-6
}()
num := <-c
fmt.Println(num)
fmt.Println("main協(xié)程結(jié)束")
}
for range 遍歷信道
for range 循環(huán)用于在一個信道關(guān)閉之前,從信道接收數(shù)據(jù)驼仪。
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for {
v, ok := <-ch
if ok == false {
break
}
fmt.Println("Received ", v, ok)
}
}
阻塞性質(zhì)
Channel 的讀取和寫入操作在各自的協(xié)程內(nèi)部都是阻塞的掸犬。意思就是,如果管道滿了绪爸,一個對channel放入數(shù)據(jù)的操作就會阻塞湾碎,直到有某個routine從channel中取出數(shù)據(jù),這個放入數(shù)據(jù)的操作才會執(zhí)行奠货。相反同理介褥,如果管道是空的,一個從channel取出數(shù)據(jù)的操作就會阻塞递惋,直到某個routine向這個channel中放入數(shù)據(jù)柔滔,這個取出數(shù)據(jù)的操作才會執(zhí)行。
func main() {
ch := make(chan int, 3)
ch <- 1
fmt.Println("發(fā)送數(shù)據(jù)1")
ch <- 2
fmt.Println("發(fā)送數(shù)據(jù)2")
ch <- 3
fmt.Println("發(fā)送數(shù)據(jù)3")
ch <- 4 //這一行操作就會發(fā)生阻塞丹墨,因為前三行的放入數(shù)據(jù)的操作已經(jīng)把channel填滿了
fmt.Println("發(fā)送數(shù)據(jù)4")
}
主routine要向channel中放入一個數(shù)據(jù)廊遍,但是因為channel沒有緩沖,相當(dāng)于channel一直都是滿的贩挣,所以這里會發(fā)生阻塞,主routine在這里一阻塞喉前,造成死鎖!
func main() {
ch := make(chan int)
<-ch //這一行會發(fā)生阻塞王财,因為channel才剛創(chuàng)建卵迂,是空的,沒有東西可以取出
}
從這里可以看出绒净,對于無緩沖的channel见咒,放入操作和取出操作不能再同一個routine中,而且應(yīng)該是先確保有某個routine對它執(zhí)行取出操作挂疆,然后才能在另一個routine中執(zhí)行放入操作改览。
select
超時控制
time.After方法下翎,它返回一個類型為<-chan Time的單向的channel,在指定的時間發(fā)送一個當(dāng)前時間給返回的channel中宝当。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
time.Sleep(2 * time.Second)
c <- 1
}()
select {
case res := <-c:
fmt.Println("result", res)
case <-time.After(1 * time.Second):
fmt.Println("Timeout")
}
}