channel是GO語言併發體系中的主推的通信機制,它可以讓一個 goroutine 通過它給另一個 goroutine 發送值信息。每個 channel 都有一個特殊的類型,也就是 channels 可發送數據的類型。一個可以發送 int 類型數據的 channel 一般寫爲 chan int。Go語言提倡使用通信的方法代替共享內存,當一個資源需要在 goroutine 之間共享時,通道在 goroutine 之間架起了一個管道,並提供了確保同步交換數據的機制。聲明通道時,需要指定將要被共享的數據的類型。可以通過通道共享內置類型、命名類型、結構類型和引用類型的值或者指針。
channel是一種特殊的類型是保證協程安全的,也就是在任何時候,同時只能有一個 goroutine 訪問通道進行發送和獲取數據。而且遵循先入先出(First In First Out)的規則,保證收發數據的順序。這兩個特性是channel可以產生共享內存功能的重要原因。。讀完這一講,下面我們就可以繼續我們的例子,開始GO語言併發的實戰了
一、通道的聲明
1.經典方式聲明
通過使用chan類型,其聲明方式如下:
var name chan type
其中type表示通道內的數據類型;name:通道的變量名稱,不過這樣創建的通道只是空值 nil,一般來說都是通道都是通過make函數創建的。
2.make方式
make函數可以創建通道格式如下:
name := make(chan type)
3.創建帶有緩衝的通道
後面會講到緩衝通道的概念,這裏先說他的定義方式
name := make(chan type, size)
其中type表示通道內的數據類型;name:通道的變量名稱,size代表緩衝的長度。
二、通道的數據收發
1. 通道的數據發送
通道的發送的操作符<-,將數據通過通道發送的格式爲:
chan <- value
注意,如果將數據發送至一個無緩衝的通道中,如果數據一直都沒有接收,那麼發送操作將持續阻塞。但是GO的編譯器能夠發現明顯的錯誤,比如
ch := make(chan int) // 創建一個整型通道
ch <- 0// 嘗試將0通過通道發送
編譯時會報錯:fatal error: all goroutines are asleep - deadlock!
2.通道的數據接收
通道接收數據的操作符也是<-,具體有以下幾種方式
1) 阻塞接收數據
阻塞模式接收數據時,將接收變量作爲<-操作符的左值,格式如下:
data := <-ch
執行該語句時將會阻塞,直到接收到數據並賦值給 data 變量。
如需要忽略接收的數據,則將data變量省略,具體格式如下:
<-ch
2) 非阻塞接收數據
使用非阻塞方式從通道接收數據時,語句不會發生阻塞,格式如下:
data, ok := <-ch
非阻塞的通道接收方法可能造成高的 CPU 佔用,因此使用非常少。一般只配合select語句配合定時器做超時檢測時使用。
三、channel的超時檢測
Go語言沒有提供直接的超時處理機制,一般使用select關鍵字來設置超時。Select雖然不是專爲超時而設計的,卻能很方便的解決超時問題,因爲select的特點是隻要其中有一個 case 已經完成,程序就會繼續往下執行,而不會考慮其他 case 的情況。select 的用法與 switch 語言非常類似,由 select 開始一個新的選擇塊,每個選擇條件由 case 語句來描述。
但是與switch 語句相比,select 有比較多的限制,其中最大的一條限制就是每個 case 語句裏必須是一個 IO 操作,結構如下:
select {
case <-chan1:
// 如果chan1成功讀到數據,則進行該case處理語句
case chan2 <- 1:
// 如果成功向chan2寫入數據,則進行該case處理語句
default:
// 如果上面都沒有成功,則進入default處理流程
}
比如在示例代碼中,我們創建了一個用於傳數據的channel,和一個用於超時退出的通道quit,並使用select來接收channel的數據,其中如果程序運行到3s時,退出通道會被置爲true,這時<-quit不再阻塞,主goroutine退出。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int) //設置一個傳送數據的channel
quit := make(chan bool) //設置一個超時傳送的channel
//新開一個協程
go func() {
for {
select {
case num := <-ch: //如果收到傳輸能道ch中的值則打印.
fmt.Println("num = ", num)
case <-time.After(3 * time.Second): //如果達到3秒則認定超時
fmt.Println("超時")
quit <- true //將quit通道置爲true
}
}
}() //別忘了()
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
<-quit //當超時quit將不再阻塞,主協和將會退出
fmt.Println("程序結束")
/*運行結果爲
num = 0
num = 1
num = 2
num = 3
num = 4
超時
程序結束
*/
}
四、通道數據收發的注意事項
1.通道的收發操作在不同的兩個 goroutine 間進行。由於通道的數據在沒有接收方處理時,數據發送方會持續阻塞,因此通道的接收必定在另外一個 goroutine 中進行。
2.接收將持續阻塞直到發送方發送數據。如果接收方接收時,通道中沒有發送方發送數據,接收方也會發生阻塞,直到發送方發送數據爲止。
3.每次只接收一個元素。
第4節 深入理解GO語言中的channel
GO語言中的有關chan的代碼位置在GOPATH\src\runtime\chan.go,閱讀代碼可以發現channel 內部就是一個帶鎖的隊列。
1.基本數據結構
type hchan struct {
qcount uint // 隊列中數據個數
dataqsiz uint // channel 大小
buf unsafe.Pointer // 存放數據的環形數組
elemsize uint16 // channel 中數據類型的大小
closed uint32 // 表示 channel 是否關閉
elemtype *_type // 元素數據類型
sendx uint // send 的數組索引
recvx uint // recv 的數組索引
recvq waitq // 由 recv 行爲(也就是 <-ch)阻塞在 channel 上的 goroutine 隊列
sendq waitq // 由 send 行爲 (也就是 ch<-) 阻塞在 channel 上的 goroutine 隊列
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking. lock mutex
}
如果是一個無緩衝的chan只需要用一個 lock來確保無競爭衝突。而帶緩衝的chan其實就是一個環形隊列,通過sendx 和 recvx 分別用來記錄發送、接收的位置。
2.數據發送的實現
其實數據發送和接收的邏輯比類似,這裏我們只舉數據發送的例子。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {//正確性檢查
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, funcPC(chansend))//重置競爭標誌位
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not closed, we observe that the channel is
// not ready for sending. Each of these observations is a single word-sized read
// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
// Because a closed channel cannot transition from 'ready for sending' to
// 'not ready for sending', even if the channel is closed between the two observations,
// they imply a moment between the two when the channel was both not yet closed
// and not ready for sending. We behave as if we observed the channel at that moment,
// and report that the send cannot proceed.
//
// It is okay if the reads are reordered here: if we observe that the channel is not
// ready for sending and then observe that it is not closed, that implies that the
// channel wasn't closed during the first observation.
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false//如果隊列被關閉等情況,返回錯誤
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
if !block {
unlock(&c.lock)
return false
}
// Block on the channel. Some receiver will complete our operation for us.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
KeepAlive(ep)
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
return true
}
我們看到在發送數據時,首先獲取lock,然後進行競爭檢查、指針檢查等,然後更新讀寫位置,記錄數據,並釋放鎖,如果,當前hchan.buf 無可用空間,則將操作阻塞。
所以從本質上講channel就是一個基於鎖的循環隊列。