Golang 語言深入理解:channel
title: Golang語言筆記: 理解 Channel 特性
date: 2018-04-20 20:00:00
tags: Golang

image
理解 channel 特性
本文是對 Gopher 2017 中一個非常好的 Talk�: [Understanding Channel](GopherCon 2017: Kavya Joshi - Understanding Channels) 的學習筆記,希望能夠通過對 channel 的關鍵特性的理解,進一步掌握其用法細節以及 Golang 語言設計哲學的管窺蠡測。
文章內容包括:
[TOC]
channel
channel
是可以讓一個 goroutine 傳送特定值到另一個 gouroutine 的通訊機制。
element type make nil
如何使用 channel?
原生的 channel 是沒有快取的(unbuffered channel),可以用於 goroutine 之間實現同步。
- 傳送 sends 和接收 receives
ch := make(chan int) // ch hase type `chan int` ch <- x // a send statement x = <-ch // a receive expression in an assignment statement <-ch // a receive statement; result is discarded
- 關閉 close
close(ch)
關閉後不能再寫入,可以讀取直到 channel 中再沒有資料,並返回元素型別的零值。
-
buffered channel
的建立
ch := make(chan int) // unbuffered channel ch := make(chan int, 0) // unbuffered channel ch := make(chan int, 3) // buffered channel with capacity 3
buffered channel 可以用於非常方便的實現生產者-消費者模型,實現非同步操作。
使用 unbuffered channel 實現同步
gopl/ch3/netcat3
func main() { conn, err := net.Dial("tcp", "localhost:8008") if err != nil { log.Fatal(err) } // communication over an buffered channel causes the sending and // receiving goroutines to synchronize done := make(chan struct{}) go func() { io.Copy(os.Stdout, conn) // NOTE; ignoring errors log.Println("Done") done <- struct{}{} // signal the main goroutine }() mustCopy(conn, os.Stdin) conn.Close() <-done }
channel 的特性
- goroutine-safe
goroutine 安全 - store and pass values between goroutines
儲存資料並在 goroutine 之間傳遞資料 - provide FIFO semantics
提供 FIFO 語義 - can cause goroutines to block and unblock
可以使得 goroutine 阻塞或者釋放
這些特性是怎麼做到的?
making channels
首先從 channel 是怎麼被建立的開始:
一個 channel 的誕生

Alt text
在 heap
上分配一個 hchan
型別的物件,並將其初始化,然後返回一個指向這個 hchan
物件的指標。
-
heap
上而不是stack
上 -
hchan
型別 - 返回的是指標

Alt text
sends and receives
理解了 channel 的資料結構實現,現在轉到 channel 的兩個最基本方法: sends
和 receivces
,看一下以上的特性是如何體現在 sends
和 receives
中的:

Alt text
goroutine-safe 的實現
假設傳送方先啟動,執行 ch <- task0
:
- 獲取
lock
,加鎖; - 對
Task
型別的物件task0
執行入隊操作; - 完成入隊操作後,釋放鎖
需要特別指出的是,這裡的入隊 enqueue
操作實際上是一次 memcopy
行為,將整個 task0
複製一份到 buf
,也就是FIFO緩衝佇列中。
如此為 channel 帶來了 goroutine-safe
的特性。

Alt text
在這樣的模型裡, sender goroutine -> channel -> receiver goroutine
之間, hchan
是唯一的共享記憶體,而這個唯一的共享記憶體又通過 mutex
來確保 goroutine-safe
,所有在佇列中的內容都只是副本。
這便是著名的 golang 併發原則的體現:
不要通過共享記憶體來通訊,而是通過通訊來共享記憶體。
控制 goroutine 之間同步的實現
- 當 channel 中的快取滿了,傳送方繼續傳送,會發生什麼?
傳送方 goroutine 會阻塞,暫停,並在收到 receive
後才恢復。
- 這是怎麼做到的?
goroutine 是一種 使用者態執行緒 , 由 Go runtime 建立並管理,而不是作業系統,比起作業系統執行緒來說,goroutine更加輕量。
Go runtime scheduler 負責將 goroutine 排程到作業系統執行緒上。

Alt text
runtime scheduler 怎麼將 goroutine 排程到作業系統執行緒上?

Alt text
當阻塞發生時,一次 goroutine 上下文切換的全過程:

Alt text
- 當傳送方 goroutine 向已經滿了的 channel 傳送資料後,發生了 goroutine 的阻塞,goroutine 會對 runtime scheduler 發起一次
gopark
呼叫; - 當 sheduler 接收到
gopark
呼叫,會將現在正在執行的 goroutineG1
從running
置為waiting
; - 並將
G1
和承載它的作業系統執行緒M
之間的聯絡解除; - 從 runqueue 排程一個新的
runnable
goroutineG
,並將其和M
繫結,開始執行G
只是阻塞了 goroutine,沒有阻塞作業系統執行緒。
然而,被阻塞的 goroutine 怎麼恢復過來?

Alt text

Alt text
阻塞發生時,呼叫 runtime sheduler 執行 gopark
之前,G1 會建立一個 sudog
,並將它存放在 hchan
的 sendq
中。 sudog
中便記錄了即將被阻塞的 goroutine G1
,以及它要傳送的資料元素 task4
等等。
接收方將通過這個 sudog
來恢復 G1
接收方 G2 接收資料, 併發出一個 receivce
,將 G1 置為 runnable
:
- G2 將佇列中的第一個元素
task1
出隊(接收 task1); - 將
sendq
中的sudog
出棧,獲取到G1
和task4
,然後將task4
入隊。在這裡讓 G2 而非 G1 執行元素入隊操作的用意在於,這樣 G1 恢復執行後無需再獲取一次鎖,將元素入隊,然後再釋放一次鎖,即可以減少一次獲取釋放鎖的過程; - 接下來 G2 要將 G1 置為
runnable
。G2 向 runtime scheduler 發起一次goready
呼叫,scheduler 將 G1 置為runnable
,並將其入隊到 runqueue,然後返回 G2。

Alt text
- 當 channel 中的快取空了,接收方繼續接受,會發生什麼?

Alt text
同樣的, 接收方 G2 會被阻塞,G2 會建立 sudoq
,存放在 recvq
,基本過程和傳送方阻塞一樣。
不同的是,傳送方 G1如何恢復接收方 G2,這是一個非常神奇的實現。

Alt text
理論上可以將 task 入隊,然後恢復 G2, 但恢復 G2後,G2會做什麼呢?
G2會將佇列中的 task 複製出來,放到自己的 memory 中,基於這個思路,G1在這個時候,直接將 task 寫到 G2的 stack memory 中!

Alt text
這是違反常規的操作,理論上 goroutine 之間的 stack 是相互獨立的,只有在執行時可以執行這樣的操作。
這麼做純粹是出於效能優化的考慮,原來的步驟是:
- G1 獲取鎖,將 task 進行入隊(memcopy)
- 當 G2 恢復時,獲取鎖並且讀取 task(memcoy)
優化後,相當於減少了 G2 獲取鎖並且執行 memcopy 的效能消耗。
總結: 特性的實現
- goroutine-safe:
- 通過 hchan mutex 實現
- store values, pass in FIFO:
- 通過 hchan buffer實現
- 通過共享 hchan buffer 進行傳值(實際上是傳遞副本)
- 唯一共享的 buffer 使用 hchan mutex 保證 goroutine 安全
- can cause goroutines to pause and resume
- 通過 hchan
sudog queues
實現 - 呼叫 runtime scheduler(
gopark
,goready
)
- 通過 hchan
simplicity 和 performance 的權衡
channel 設計背後的思想可以理解為 simplicity 和 performance 之間權衡抉擇,具體如下:
Simplicity
queue with a lock prefered to lock-free implementation:
比起完全 lock-free 的實現,使用鎖的佇列實現更簡單,容易實現
The performance improvement does not materiablize from the air, it comees with code complexity increase.
效能的提升(lock-free)不是憑空實現的,它來自程式碼複雜性的增長。
Performance

Alt text