一文讀透GO語言的通道

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就是一個基於鎖的循環隊列。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章