有關golang信道的面試筆記

信道是一個goroutine之間很關鍵的通信媒介。

理解golang的信道很重要,這裏記錄平時易忘記的、易混淆的點。

1. 基本使用

剛聲明的信道,零值爲nil,無法直接使用,需配合make函數進行初始化

   ic :=  make(chan int)
   ic  <-22   // 向無緩衝信道寫入數據
   v := <-ic  // 從無緩衝信道讀取數據
  • 無緩衝信道: 一手交錢,一手交貨, sender、receiver必須同時做好動作,才能完成發送->接收;否則,先準備好的一方將會阻塞等待。
  • 有緩衝信道 make(chan int,10):滑軌流水線,因爲存在緩衝空間,故並不強制sender、receiver必須同時準備好;當通道滿時, 依舊會阻塞。

信道存在三種狀態: nil, active, closed

針對這三種狀態,sender、receiver有一些行爲,我也不知道如何強行記憶這些行爲 ☹️:

動作 nil active closed
close panic 成功 panic
ch <- 死鎖 阻塞或成功 panic
<-ch 死鎖 阻塞或成功 零值

2. 從1個例子看chan的實質

package main
 
import (
    "fmt"
)
 
func SendDataToChannel(ch chan int, value int) {
    fmt.Printf("ch's value:%v, chan's type: %T \n", ch, ch) // %v 顯示struct的值;%T 顯示類型
    ch <- value
}
 
func main() {
    var v int
    ch := make(chan int)     
    fmt.Printf("ch's value:%v, chan's type: %T \n", ch, ch) 
    go SendDataToChannel(ch, 101)         // 通過信道發送數據
    v = <-ch                              //  從信道接受數據
    fmt.Println(v)       // 101
}

能正確打印101。

Q1: 剛學習golang的時候,一直給我們灌輸golang函數是值傳遞,那上例在另外一個協程內部對形參的操作,爲什麼會影響外部的實參?

請關注格式化字符的日誌輸出:

ch's value:0xc000018180, chan's type: chan int 
ch's value:0xc000018180, chan's type: chan int 
101

A: 上面的日誌顯示傳遞的ch是一個指針值0xc000018180,類型是chan int( 這並不是說ch是指向chan int類型的指針)。

chan int本質就是指向hchan結構體的指針。

內置函數make創建信道: func makechan(t *chantype, size int) *hchan返回了指向hchan結構體的指針:

type hchan struct {
	qcount   uint           // 隊列中已有的緩存元素的長度
	dataqsiz uint           // 環形隊列的長度
	buf      unsafe.Pointer // 環形隊列的地址
	elemsize uint16
	closed   uint32
	elemtype *_type // 元素類型
	sendx    uint   // 待發送的元素索引
	recvx    uint   // 待接受元素索引
	recvq    waitq  // 阻塞等待的goroutine
	sendq    waitq  // 阻塞等待的gotoutine

	// 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
}

Q2: 緩衝信道內部爲什麼要使用環形隊列?

A:golang是使用數組來實現信道隊列,在不移動元素的情況下, 隊列會出現“假滿”的情況,


在做成環形隊列的情況下, 所有的入隊出隊操作依舊是 O(1)的時間複雜度,同時元素空間可以重複利用。
需要使用sendIndex,receIndex來標記實際的待插入/拉取位置,顯而易見會出現 sendIndex<=receIndex 的情況。

recvq,recev是由鏈表實現的隊列,用於存儲阻塞等待的goroutine和待發送/待接收值,
這兩個結構也是阻塞goroutine被喚醒的準備條件。

3. 發送/接收的細節

不要使用共享內存來通信,而是使用通信來共享內存

元素值從外界進入信道會被複制,也就是說進入信道的是元素值的副本,並不是元素本身進入信道 (出信道類似)。

金玉良言落到實處:不同的線程不共享內存、不用鎖,線程之間通訊用channel同步也用channel。
發送/接收數據的兩個動作(G1,G2,G3)沒有共享的內存,底層通過hchan結構體的buf,使用copy內存的方式進行通信,最後達到了共享內存的目的。

② 根據第①點,發送操作包括:複製待發送值,放置到信道內;
接收操作包括:複製元素值, 放置副本到接收方,刪除原值,以上行爲在全部完成之前都不會被打斷
所以第①點所說的無鎖,其實指的業務代碼無鎖,信道底層實現還是靠鎖。

以send操作爲例,下面代碼截取自 https://github.com/golang/go/blob/master/src/runtime/chan.go#L216

if c.qcount < c.dataqsiz {
  	// Space is available in the channel buffer. Enqueue the element to send.
  	qp := chanbuf(c, c.sendx)         // 計算出buf中待插入位置的地址
  	if raceenabled {
  		racenotify(c, c.sendx, nil)
  	}
  	typedmemmove(c.elemtype, qp, ep)  // 將元素copy進指定的qp地址
  	c.sendx++                         // 重新計算待插入位置的索引
  	if c.sendx == c.dataqsiz {
  		c.sendx = 0                      
  	}
  	c.qcount++
  	unlock(&c.lock)
  	return true
  }

一個常規的send動作:

  • 計算環形隊列的待插入位置的地址
  • 將元素copy進指定的qp地址
  • 重新計算待插入位置的索引sendx
  • 如果待插入位置==隊列長度,說明插入位置已到尾部,需要插入首部。
  • 以上動作加鎖

進入等待狀態的goroutine會進入hchan的sendq/recvq列表

調度器將G1、G2置爲waiting狀態,G1、G2進入sendq列表,同時與邏輯處理器分離;

直到有G3嘗試讀取信道內recvx元素,之後將喚醒隊首G1進入runnable狀態,加入調度器的runqueue。

這裏面涉及gopark, goready兩個函數。

如果是無緩衝信道引起的阻塞,將會直接拷貝G1的待發送值到G2的存儲位置

✍️ https://github.com/golang/go/blob/master/src/runtime/chan.go#L527

package main

import (
	"fmt"
	"time"
)

func SendDataToChannel(ch chan int, value int) {
	time.Sleep(time.Millisecond * time.Duration(value))
	ch <- value
}

func main() {
	var v int
	var ch chan int = make(chan int)
	go SendDataToChannel(ch, 104) // 通過信道發送數據
	go SendDataToChannel(ch, 100) // 通過信道發送數據
	go SendDataToChannel(ch, 95)  // 通過信道發送數據
	go SendDataToChannel(ch, 120) // 通過信道發送數據

	time.Sleep(time.Second)
	v = <-ch       //  從信道接受數據
	fmt.Println(v)  

	time.Sleep(time.Second * 10)
}

Q3:上述代碼大概率穩定輸出95

A:雖然4個goroutine被啓動的順序不定,但是肯定都阻塞了,阻塞的時機不一樣,被喚醒的是sendq隊首的goroutine,基本可認爲第三個goroutine被首先捕獲進sendq ,應爲是無緩衝信道,將會直接拷貝G3的95給到待接收地址。

4. 業內總結的信道的常規姿勢

無緩衝、緩衝信道的特徵,已經在golang領域形成了特定的套路。

  • 當容量爲0時,說明信道中不能存放數據,在發送數據時,必須要求立馬有人接收,此時的信道稱之爲無緩衝信道。

  • 當容量爲1時,說明信道只能緩存一個數據,若信道中已有一個數據,此時再往裏發送數據,會造成程序阻塞,利用這點可以利用信道來做鎖。

  • 當容量大於1時,信道中可以存放多個數據,可以用於多個協程之間的通信管道,共享資源。

Q4: 爲什麼無緩衝信道不適合做鎖?

A: 我們先思考一下鎖的業務實質: 獲取獨佔標識,並能夠繼續執行; 無緩衝信道雖然可以獲取獨佔標識,但是他阻塞了自身goroutine的執行,所以並不適合實現業務鎖。

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