Golang學習筆記:channel


channel

channel是goroutine之間的通信機制,它可以讓一個goroutine通過它給另一個goroutine發送數據,每個channel在創建的時候必須指定一個類型,指定的類型是任意的。
使用內置的make函數,可以創建一個channel類型:

ch := make(chan int)

發送和接受

channel主要的操作有發送和接受:

// 發送數據到channel
ch <- 1
// 從channel接受數據
x := <- ch

如向channel發送數據的時候,該goroutine會一直阻塞直到另一個goroutine接受該channel的數據,反之亦然,goroutine接受channel的數據的時候也會一直阻塞直到另一個goroutine向該channel發送數據,如下面操作:

func main() {
    ch := make(chan string)
    // 在此處阻塞,然后程序會彈出死鎖的報錯
    c <- "hello"
    fmt.Println("channel has send data")
}

正確的操作:

func main() {
    ch := make(chan string)
    go func(){
        // 在執行到這一步的時候main goroutine才會停止阻塞
        str := <- ch
        fmt.Println("receive data:" + str)
    }()
    ch <- "hello"
    fmt.Println("channel has send data")
}

說到channel的阻塞,就不得不說到有緩沖的channel。

帶緩沖的channel

帶緩沖的channel的創建和不帶緩沖的channel(也就是上面用的channel)的創建差不多,只是在make函數的第二個參數指定緩沖的大小。

// 創建一個容量為10的channel
ch := make(chan int, 10)

帶緩沖的channel就像一個隊列,遵從先進先從的原則,發送數據向隊列尾部添加數據,從頭部接受數據。
image

goroutine向channel發送數據的時候如果緩沖還沒滿,那么該goroutine就不會阻塞。

ch := make(chan int, 2)
// 前面兩次發送數據不會阻塞,因為緩沖還沒滿
ch <- 1
ch <- 2
// goroutine會在這里阻塞
ch <- 3

反之如果接受該channel數據的時候,如果緩沖有數據,那么該goroutine就不會阻塞。

channel與goroutine之間的應用可以想象成某個工廠的流水線工作,流水線上面有打磨,上色兩個步驟(兩個goroutine),負責打磨的工人生產完成后會傳給負責上色的工人,上色的生產依賴於打磨,兩個步驟之間的可能存在存放槽(channel),如果存放槽存滿了,打磨工人就不能繼續向存放槽當中存放產品,直到上色工人拿走產品,反之上色工人如果把存放槽中的產品都上色完畢,那么他就只能等待新的產品投放到存放槽中。

備注

其實在實際應用中,帶緩沖的channel用的並不多,繼續拿剛才的流水線來做案例,如果打磨工人生產速度比上色工人工作速度要快,那么即便再多容量的channel,也會遲早被填滿然后打磨工人會被阻塞,反之如果上色工人生產速度大於打磨工人速度,那么有緩沖的channel也是一直處於沒有數據,上色工人很容易長時間處於阻塞的狀態。

因此比較好的解決方法還是針對生產速度較慢的一方多加人手,也就是多開幾個goroutine來進行處理,有緩沖的channel最好用處只是拿來防止goroutine的完成時間有一定的波動,需要把結果緩沖起來,以平衡整體channel通信。

單方向的channel

使用channel來使不同的goroutine去進行通信,很多時候都和消費者生產者模式很相似,一個goroutine生產的結果都用channel傳送給另一個goroutine,一個goroutine的執行依賴與另一個goroutine的結果。
因此很多情況下,channel都是單方向的,在go里面可以把一個無方向的channel轉換為只接受或者只發送的channel,但是卻不能反過來把接受或發送的channel轉換為無方向的channel,適當地把channel改成單方向,可以達到程序強約束的做法,類似於下面例子:

fuc main(){
    ch := make(ch chan string)
    
    go func(out chan<- string){
        out <- "hello"
    }(ch)
    
    go func(in <-chan string){
        fmt.Println(in)
    }(ch)
    
    time.Sleep(2 * time.Second)
}

select多路復用

在一個goroutine里面,對channel的操作很可能導致我們當前的goroutine阻塞,而我們之后的操作都進行不了。而如果我們又需要在當前channel阻塞進行其他操作,如操作其他channel或直接跳過阻塞,可以通過select來達到多個channel(可同時接受和發送)復用。如下面我們的程序需要同時監聽多個頻道的信息:

broadcaster1 := make(chan string) // 頻道1
broadcaster2 := make(chan string) // 頻道2
select {
    case mess1 := <-broadcaster1:
        fmt.Println("來自頻道1的消息:" + mess1)
    case mess2 := <-broadcaster2:
        fmt.Println("來自頻道2的消息:" + mess2)
    default:
        fmt.Println("暫時沒有任何頻道的消息,請稍后再來~")
        time.Sleep(2 * time.Second)
}

select和switch語句有點相似,找到匹配的case執行對應的語句塊,但是如果有兩個或以上匹配的case語句,那么則會隨機選擇一個執行,如果都不匹配就會執行default語句塊(如果含有default的部分的話)。
值得注意的是,select一般配合for循環來達到不斷輪詢管道的效果,可能很多小伙伴想着寫個在某個case里用break來跳出for循環,這是不行的,因為break只會退出當前case,需要使用return來跳出函數或者弄個標志位標記退出

var flag = 0
for {
	if flag == 1 {break}
	select {
		case message := <- user.RecMess :
			event := gjson.Get(string(message), "event").String()
			if event == "login" {
				Login(message, user)
			}
			break
		case <- user.End :
			flag = 1
			break
	}
}
	    

關閉

channel可以接受和發送數據,也可以被關閉。

close(ch)

關閉channel后,所有向channel發送數據的操作都會引起panic,而被close之后的channel仍然可以接受之前已經發送成功的channel數據,如果數據全部接受完畢,那么再從channel里面接受數據只會接收到零值得數據。

channel的關閉可以用來操作其他goroutine退出,在運行機制方面,goroutine只有在自身所在函數運行完畢,或者主函數運行完畢才會打斷,所以我們可以利用channel的關閉作為程序運行入口的一個標志位,如果channel關閉則停止運行。

無法直接讓一個goroutine直接停止另一個goroutine,但可以使用通信的方法讓一個goroutine停止另一個goroutine,如下例子就是程序一邊運行,一邊監聽用戶的輸入,如果用戶回車,則退出程序。

func main() {
    shutdown := make(chan struct{})
    var n sync.WaitGroup
    n.Add(1)
    go Running(shutdown, &n) 
    n.Add(1)
    go ListenStop(shutdown, &n) 
    n.Wait()
}

func Running(shutdown <-chan struct{}, n *sync.WaitGroup) {
    defer n.Done()
    for {
        select {
        case <-shutdown:
            // 一旦關閉channel,則可以接收到nil。
            fmt.Println("shutdown goroutine")
            return
        default:
            fmt.Println("I am running")
            time.Sleep(1 * time.Second)
        }   
    }   
}

func ListenStop(shutdown chan<- struct{}, n *sync.WaitGroup) {
    defer n.Done()
    os.Stdin.Read(make([]byte, 1)) 
    // 如果用戶輸入了回車則退出關閉channel
    close(shutdown)
}

利用channel關閉時候的傳送的零值信號可以有效地退出其他goroutine,特別是關閉多個goroutine的時候,就不需要向channel傳輸多個信息了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM