Channel
-
從根本上來說,channel只是一個數據結構,可以被寫入數據,也可以被讀取數據
- 所謂的發送數據到channel,或者從channel讀取數據,說白了就是對一個數據結構的操作
- 因爲協程原則上不會出現多線程編程中經常遇到的資源競爭問題,所以這個channel的數據結
構甚至在訪問的時候都不用加鎖- 因爲Go語言支持多CPU核心併發執行多個goroutine,會造成資
源競爭,所以在必要的位置還是需要加鎖的
- 因爲Go語言支持多CPU核心併發執行多個goroutine,會造成資
-
通道可以傳輸 int, string, 結構體,甚至是函數
-
通道傳遞是拷貝值
- 對於大數據類型,可以傳遞指針以避免大量拷貝
- 注意此時的併發安全,即多個goroutine通過指針對原始值的併發操作
- 此時需要額外的同步操作(例如鎖)來避免競爭
- 對於大數據類型,可以傳遞指針以避免大量拷貝
-
channel可以用來無鎖編程,但是channel本身底層還是通過加鎖實現的
-
channel是面向同一個進程的
- 多個進程之間內存一般不會共享,所以沒法用channel
- 進程間通訊可以考慮IPC方法,比如有名管道(
os.Pipe
函數),還有強大和靈活的的socket - 如果通道要給外部使用,或者通過通道對外提供功能,那就不要傳指針值了,容易造成安全漏洞
-
ch := make(chan bool)
- 無緩存的channel是同步的,阻塞式的
- 必須等待兩邊都準備好纔開始傳遞數據,否則堵塞
- 使用不當容易引發死鎖
- 無緩存的channel是同步的,阻塞式的
-
ch := make(chan int, 10)
- 有緩存的channel是異步的,只有當緩衝區寫滿時才堵塞
-
通道的關閉
- channel 使用完後不關閉也沒有關係
- 因爲channel 沒有被任何協程用到後會被自動回收
- 顯示關閉 channel 一般是用來通知其他協程某個任務已經完成了
- 關閉一個已經關閉的channel,或者往一個已經關閉的channel寫入,都會panic
- 應該是生產者關閉通道,而不是消費者
- 否則生產者可能在channel關閉後寫入,導致panic
- 消費者在超時後應該通過channel向生產者發送完成消息,讓生產者關閉channel並返回
- 已經被關閉的通道不能往裏面寫入,但可以接受數據
- 讀取一個已經關閉且沒有數據的通道會立刻返回一個零值
- 讀取時通過判斷第二個返回值也可以判讀收到的值是否有效
- channel 使用完後不關閉也沒有關係
-
單向通道
- 多用於函數的參數, 提高安全性
- 只能寫入:
var send chan<- int
- 只能讀取:
var recv <-chan int
-
使用for-range循環讀取channel
- 從指定通道中讀取數據直到通道關閉(close)
- 如果生產者忘記關閉通道,則消費者會一直堵塞在for-range循環中
- 如果
ch
值爲nil
,則會那麼這條for語句就會被永遠地阻塞在有for關鍵字的那一行
例子
- 用channel進行同步
ype Stream struct {
// some fields
cc chan struct{}
}
func (s *Stream) Wait() error {
<-s.cc
// some code
}
func (s *Stream) Close() {
// some code
close(s.cc)
}
func (s *Stream) IsClosed() bool {
select {
case <-s.cc:
return true
default:
return false
}
}
- 用channel限制速度
limiter := time.Tick(time.Millisecond * 200)
// 每 200ms 執行一次請求
for req := range requests {
<-limiter
...
}
- 臨時速度限制
- 就是說只限制前幾次請求,而不限制後續請求速度
const N = 3 // 限制前三次請求
limiter := make(chan struct{}, N)
// 先把通道堵塞
for i := 0; i < N; i++ {
limiter <- struct{}
}
// 前三次請求限制爲200毫秒一次
go func() {
for t := range time.Tick(time.Millisecond * 200) {
limiter <- struct{}
}
}()
for req := range {
<-limiter
// 業務邏輯
}
- channel作爲函數返回值
- 一般作爲生產者,另起一個goroutine併發生產,返回channel用於消費
- 注意點在於使用defer把goroutine的生命週期封裝在生產函數中
- 目的在於避免寫入nil或者多次關閉channel
- 消費者只需要處理阻塞和零值
- 生產者負責在生產完畢後關閉channel
func producer(num int) <-chan {
ch := make(chan int, num)
go func() {
defer close(ch) // 重要!
for i:=0; i< num; i++ {
ch <- i
}
}()
return ch
}
- 實現信號量
var wg sync.WaitGroup
sem := make(chan struct{}, 5) // 最多併發5個
for i := 0; i < 100; i++ {
wag.Add(1)
go func(id int) {
return wg.Done()
sem <- struct{} // 獲取信號量
defer func(){
<-sem // 釋放信號量
}()
// 業務邏輯
...
}(i)
}
wg.Wait()
Select
-
select語句是專爲通道而設計的,所以每個case表達式中都只能包含操作通道的表達式
-
多個channel準備好時,隨機選一個執行
-
select語句包含的候選分支中的case表達式都會在該語句執行開始時先被求值
- 求值的順序是依從代碼編寫的順序從上到下
- 如果表達式在被求值時,相應的操作正處於阻塞狀態,那麼對該case表達式的求值就是不成功的,即這個case表達式所在的候選分支是不滿足選擇條件的
-
select 默認阻塞,只有監聽的channel中有發送或者接受數據時才運行
- 設置default則不阻塞,通道內沒有待接受的數據則執行default
- 如果不加default,則會有死鎖風險
-
select語句只能對其中的每一個case表達式各求值一次,如果我們想連續或定時地操作其中的通道的話,就需要通過在for語句中嵌入select語句的方式實現
- 注意簡單地在select語句的分支中使用break語句,只能結束當前的select語句的執行,而並不會對外層的for語句產生作用
- 這種錯誤的用法可能會讓這個for語句無休止地運行下去
-
利用channel+select來廣播退出信息
- 每個子goroutine利用select監聽done通道
- 當主程序想要關閉子goroutine時,可以關閉done通道
- 此時select會立刻監聽到nil消息,子goroutine可以以此退出
func Generate(done chan bool) chan int {
ch := make(chan int)
go func() {
defer close(ch)
for {
select{
case ch <- rand.Int():
...
case <- done: // 接受到通知並退出
return
}
}
}()
return ch
}
done := make(chan bool)
ch := Generate(done)
fmt.Println(<-ch) // 消費
close(done) //通過關閉通道來發送通知
- 如果在select語句中發現某個通道已關閉,那麼應該怎樣屏蔽掉它所在的分支?
- 發現某個channel被關閉後,爲了防止再次進入這個分支,可以把這個channel重新賦值爲nil,這樣這個case就一直被阻塞了
for {
select {
case _, ok := <-ch1:
if !ok {
ch1 = nil
}
case ... :
...
default:
...
}
}
- 單個case的化簡寫法
// bad
select {
case <-ch:
}
// good
<-ch
// bad
for {
select {
case x := <-ch:
_ = x
}
}
//good
for x := range ch {
...
}