深度剖析channel

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。##channel的用法channel是golang中很重要的概念,配合goroutine是golang能夠方便實現並發編程的關鍵。channel其實就是傳統語言的阻塞訊息佇列,可以用來做不同goroutine之間的訊息傳遞,由於goroutine是輕量級的線程能夠在語言層面調度,所以channel在golang中也常被用來同步goroutine。一般channel的聲明形式為:var chanName chan ElementTypeElementType指定這個channel所能傳遞的元素類型。定義一個channel也很簡單,直接使用內建的函數make()即可:ch := make(chan int,bufferSize) //bufferSize為緩衝區的大小,可以不傳遞該值代表不帶緩衝區的channel ###訊息傳遞帶有緩衝區的channel一般用來做不同goroutine之間的訊息傳遞。最經典的解釋莫過於生產者-消費者了。生產者向channel中寫資料,如果channel緩衝區已滿,則生產者會被阻塞直到消費者消費緩衝區中的資料後才能被喚醒。消費者從channel中讀取資料,如果緩衝區中沒有任何資料則消費者會阻塞直到生產者將資料寫入才能被喚醒。假設我們有30個學生做作業,做完作業後由一個老師批改作業。用go怎麼實現呢,我們首先定義一個帶有緩衝區HomeWork chan(緩衝區的大小與學生數目相同,主要是為了防止學生提交作業時阻塞),學生做完作業向hwChan中發送資料,老師等待hwChan中有資料就取出學生作業然後批改。```const StudentNum = 30 type HomeWork struct { }function student(hwChan chan HomeWork) {//學生提交作業hwChan <- HomeWork{} }function teacher(hwChan chan HomeWork){//老師取出30個學生的作業批改for i:=0;i<StudentNum;i++ {hw := <- hwChan }}func main(){hwChan := make(chan HomeWork,30)//每個學生開啟一個goroutine,學生單獨做作業,做完作業提交到hwChan中即可for i:=0;i<StudentNum;i++ {go student(hwChan)}//老師開啟一個goroutine,去批改學生作業go teacher(hwChan)time.Sleep(5*time.Second)}```**單向channel**student只需要向channel寫資料,而不能從channel中讀資料,但在func student(hwChan chan HomeWork) 這個函數中學生其實是有許可權從channel中讀資料,能限制這個函數只能取資料而不能寫入資料嗎?單向channel就是為瞭解決這個問題的:chan<- ElementType 表示只能向這個channel寫資料<-chan ElementType 表示只能從這個channel中讀資料我們將student函數的參數變一下 func student(hwChan chan<- HomeWork)就可以將hwChan這個雙向channel變成一個只能寫的單向channel傳到student函數中。同理teacher函數的參數變成 function teacher(hwChan <-chan HomeWork)也就規定了在teacher函數內只能從這個channel中讀資料。###同步main函數最後一段代碼 time.Sleep(5*time.Second),主要是為了防止main函數所在的主goroutine沒有等teacher所在的goroutine批改完30個學生作業就退出了,主goroutine的退出就導致整個程式的退出。Sleep 5秒是因為教師肯定能在五秒內批改完作業,但是如果超過5s還是會有問題。我們現在有了新的需求,主goroutine開啟了一個teacher goroutine,主goroutine需要等待teacher批改完30個學生的作業才能繼續運行。這時我們就需要再建立一個channel用於goroutine之間的同步了。``` func main(){ hwChan := make(chan HomeWork,30) exitChan := make(chan struct{}) for i:=0;i<StudentNum;i++ {go student(hwChan)}go teacher(hwChan,exitChan)<-exitChan } function teacher(hwChan chan HomeWork,exitChan chan struct{}){for i:=0;i<StudentNum;i++ {hw := <- hwChan }close(exitChan)} ```主goroutine會阻塞在 <-exitChan,直到teacher批改完三十個學生的作業,然後close(exitChan),<-exitChan返回時teacher已批改完學生作業,程式退出也就沒有任何問題了。> 至於為什麼定義 exitChan為 chan struct{}而不是chan int{} 等是因為chan struct{}不需要佔用任何記憶體空間 var s struct{}fmt.Println(unsafe.Sizeof(s)) // prints 0##特殊狀態的channel### 未初始化的channel``` var ch chan int ch<-1 //永遠阻塞 <- ch //永遠阻塞 close(ch)//panic close of nil channel```往一個未初始化的channel中讀寫資料都會導致該goroutine永遠阻塞。> 至於為什麼要永遠阻塞而不是報panic,我目前還沒有找到比較信服的答案。### 已關閉的channel+ 往已關閉的channel中寫資料會導致panic。+ 關閉一個已關閉的channel會導致panic。+ 從已關閉的channel中讀資料不會導致panic,會返回該資料類型的零值``` ch := make(chan int) close(ch) i <- ch //i =0 close(ch) //panic: close of closed channel```但是我們不能因為從一個channel中讀出了零值就判斷該channel被關閉了,因為零值也可能是正常的資料。``` ch := make(chan int) ch<-0 //向channel中寫入0 i <- ch //雖然從channel中讀出的資料是0,但不能以此判斷channel已被關閉。 ```從channel中讀資料支援兩個傳回值得形式 i,ok <- ch ok是一個bool值。+ 從已關閉的帶有緩衝的channel中讀資料``` ch := make(chan int,3) ch <-1 ch <-0 ch <-3 i,ok <- ch //i為1,ok為true(channel未關閉) close(ch) j,ok <- ch //j是0,ok為true(channel雖然被關閉了,但是緩衝中仍然有資料,j為0並不能判斷出channel已關閉) m,ok <- ch //m是3不是0,ok為true(channel雖然被關閉了,但是緩衝中仍然有資料) n,ok <-ch //n是0,ok為false (表示channel關閉並且緩衝區中沒有資料了,n被賦值為該類型的零值)```在通道被關閉之後並不會立即把false作為相應接收操作的第二個結果,而是等到接收端把已在通道中的所有元素值都接收到之後才這樣做。這樣就保證了channel關閉之前發送到channel的資料都能夠被消費端讀取出來。讀取channel中的元素還有一種更簡便的方法``` for elem <- range ch { } //當ch被關閉並且緩衝中的資料都被取出後,for迴圈會退出```## 源碼實現```//一個channel其實是一個hchan變數type hchan struct {buf unsafe.Pointer // 指向緩衝區的指標,如果緩衝區為空白則為nildataqsiz uint // 緩衝區的大小elemtype *_type // element typeelemsize uint16 //單個元素的size//sendx recvx 實現一個FIFO的數組列表sendx uint // send indexrecvx uint // receive indexqcount uint // 緩衝區中緩衝的元素數量recvq waitq // 消費者等待列表sendq waitq // 生產者等待列表closed uint32 //標識channel是否關閉lock mutex}//初始化一個channel func makechan(t *chantype, size int64) *hchan {elem := t.elemvar c *hchanif size == 0 { //初始化一個不帶緩衝區的channelc = (*hchan)(mallocgc(hchanSize+uintptr(size)*uintptr(elem.size), nil, flagNoScan))} else { //初始化一個帶緩衝區的channel,需要建立一個數組做緩衝區c = new(hchan)c.buf = newarray(elem, uintptr(size))}c.elemsize = uint16(elem.size)c.elemtype = elemc.dataqsiz = uint(size)return c}//往channel中寫入資料func chansend(t *chantype, c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {if c == nil {//往一個未初始化的channel(nil)寫資料會導致goroutine永久阻塞gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)throw("unreachable")}//加鎖,所以多個gorotuine同時向一個channel中讀寫資料是不會有問題的lock(&c.lock)//往一個已經關閉的channel寫資料會導致panicif c.closed != 0 {unlock(&c.lock)panic("send on closed channel")}//根據緩衝區的大小dataqsiz,判斷是否存在緩衝區if c.dataqsiz == 0 {//試著從recvq等待隊列裡取出一個等待的goroutinesg := c.recvq.dequeue()if sg != nil {// 找到一個等待讀取資料的goroutine(消費者),將要寫入的元素直接交給(拷貝)這個goroutine,然後再將這個拿到元素的goroutine給設定為ready狀態unlock(&c.lock)recvg := sg.g //recvg指向下一個消費者syncsend(c, sg, ep) //將要寫入的元素直接交給(拷貝)這個消費者goready(recvg, 3)return true}// 如果recvq裡,並沒有一個等待的goroutine,那麼就將待寫入的元素儲存在當前執行寫的goroutine的結構裡,然後將當前goroutine入隊到sendq中並被掛起,等待有人來讀取元素後才會被喚醒mysg := acquireSudog()mysg.elem = ep //生產者自己儲存資料c.sendq.enqueue(mysg)goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)unlock(&c.lock)return true}// 如果帶有緩衝區的channel,則生產者要等待緩衝區中有閒置空間存放生產者所生產的資料// qcount是緩衝區已有資料的數量,dataqsiz是可存放資料的數量for futile := byte(0); c.qcount >= c.dataqsiz; futile = traceFutileWakeup {//如果緩衝區沒有可用空間,就將當前goroutine入隊到sendq中並被掛起等待。mysg := acquireSudog()mysg.elem = nilc.sendq.enqueue(mysg)goparkunlock(&c.lock, "chan send", traceEvGoBlockSend|futile, 3)unlock(&c.lock)}//如果緩衝區有可用空間,就將元素追加到緩衝區中,再從消費者等待隊列recvq中試著取出一個等待的goroutine開始進行讀操作(如果recvq中有等待讀的goroutine的話)typedmemmove(c.elemtype, chanbuf(c, c.sendx), ep)c.sendx++if c.sendx == c.dataqsiz {c.sendx = 0}c.qcount++// wake up a waiting receiversg := c.recvq.dequeue()if sg != nil {recvg := sg.gunlock(&c.lock)goready(recvg, 3)} else {unlock(&c.lock)}return true}//從channel中讀資料的過程和寫過程差不多就不再貼代碼了```##參考資料channel in Go’s runtime (skoo的部落格,乾貨滿滿) 《Go語言編程》 《Go並發編程實戰》

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.