golang channel和select

Channel

  • 從根本上來說,channel只是一個數據結構,可以被寫入數據,也可以被讀取數據

    • 所謂的發送數據到channel,或者從channel讀取數據,說白了就是對一個數據結構的操作
    • 因爲協程原則上不會出現多線程編程中經常遇到的資源競爭問題,所以這個channel的數據結
      構甚至在訪問的時候都不用加鎖
      • 因爲Go語言支持多CPU核心併發執行多個goroutine,會造成資
        源競爭,所以在必要的位置還是需要加鎖的
  • 通道可以傳輸 int, string, 結構體,甚至是函數

  • 通道傳遞是拷貝值

    • 對於大數據類型,可以傳遞指針以避免大量拷貝
      • 注意此時的併發安全,即多個goroutine通過指針對原始值的併發操作
      • 此時需要額外的同步操作(例如鎖)來避免競爭
  • channel可以用來無鎖編程,但是channel本身底層還是通過加鎖實現的

  • channel是面向同一個進程的

    • 多個進程之間內存一般不會共享,所以沒法用channel
    • 進程間通訊可以考慮IPC方法,比如有名管道(os.Pipe函數),還有強大和靈活的的socket
    • 如果通道要給外部使用,或者通過通道對外提供功能,那就不要傳指針值了,容易造成安全漏洞
  • ch := make(chan bool)

    • 無緩存的channel是同步的,阻塞式的
      • 必須等待兩邊都準備好纔開始傳遞數據,否則堵塞
    • 使用不當容易引發死鎖
  • ch := make(chan int, 10)

    • 有緩存的channel是異步的,只有當緩衝區寫滿時才堵塞
  • 通道的關閉

    • channel 使用完後不關閉也沒有關係
      • 因爲channel 沒有被任何協程用到後會被自動回收
      • 顯示關閉 channel 一般是用來通知其他協程某個任務已經完成了
    • 關閉一個已經關閉的channel,或者往一個已經關閉的channel寫入,都會panic
    • 應該是生產者關閉通道,而不是消費者
      • 否則生產者可能在channel關閉後寫入,導致panic
      • 消費者在超時後應該通過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 {
   ...
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章