golang select 機制

來源: 愛發白日夢的後端

在 Go 語言中,select 是一種用於處理多個通道操作的控制結構。它可以用於在多個通道之間進行非阻塞的選擇操作。

select 語句由一系列的 case 子句組成,每個 case 子句表示一個通道操作。select 語句會按照順序依次檢查每個 case 子句,並執行其中可執行的操作。

select 的作用主要有以下幾個方面:

多路複用通道

select 可以同時監聽多個通道上的操作,一旦某個通道可讀或可寫,就會執行相應的操作。這樣可以避免使用阻塞的 channel 操作,提高程序的併發性能。

package main

import (
 "fmt"
 "time"
)

func main() {
 ch1 := make(chan int)
 ch2 := make(chan int)

 go func() {
  time.Sleep(2 * time.Second)
  ch1 <- 1
 }()

 go func() {
  time.Sleep(1 * time.Second)
  ch2 <- 2
 }()

 select {
 case <-ch1:
  fmt.Println("Received from ch1")
 case <-ch2:
  fmt.Println("Received from ch2")
 case <-time.After(3 * time.Second):
  fmt.Println("Timeout")
 }
}

在這個示例中,我們創建了兩個通道 ch1 和 ch2。然後分別在兩個 goroutine 中進行操作,通過不同的延遲時間向通道發送數據。

在 main 函數中,我們使用 select 語句同時監聽 ch1 和 ch2 兩個通道,並通過 <-ch1 和 <-ch2 分別接收通道中的數據。同時,我們還使用 time.After 函數設置了一個 3 秒的超時時間。

在 select 語句的執行過程中,會依次檢查每個 case 子句。如果有多個 case 子句都是可執行的,select 會隨機選擇一個執行。在這個示例中,由於 ch2 的數據發送時間比 ch1 早,所以最終會執行 case <-ch2 分支,輸出 "Received from ch2"。

如果 select 語句中的所有通道都沒有數據可讀,並且超過了設置的超時時間,那麼就會執行 time.After 對應的 case 分支,輸出 "Timeout"。

非阻塞的通道操作

select 語句中的 case 子句可以使用非阻塞的通道操作,包括髮送和接收操作。如果沒有可用的通道操作,select 會立即執行 default 子句(如果有),或者阻塞等待第一個可執行的操作。

package main

import (
 "fmt"
)

func main() {
 ch := make(chan int, 2)

 ch <- 1 // 向通道寫入數據,此時通道未滿,操作不會被阻塞
 fmt.Println("Data written to channel")

 select {
 case ch <- 2: // 嘗試向已滿的通道再次寫入數據,由於通道已滿,操作會被立即返回
  fmt.Println("Data written to channel")
 default:
  fmt.Println("Channel is full, unable to write data")
 }

 data, ok := <-ch // 嘗試從通道讀取數據,此時通道中有數據,操作不會被阻塞
 if ok {
  fmt.Println("Data read from channel:", data)
 }

 select {
 case data, ok := <-ch: // 嘗試從空的通道讀取數據,由於通道爲空,操作會被立即返回
  if ok {
   fmt.Println("Data read from channel:", data)
  } else {
   fmt.Println("Channel is empty, unable to read data")
  }
 default:
  fmt.Println("Channel is empty, unable to read data")
 }
}

在這個示例中,我們首先創建了一個緩衝大小爲 2 的通道 ch。然後,我們使用帶緩衝的通道進行數據寫入操作 ch <- 1,由於通道未滿,操作不會被阻塞。

接下來,我們使用非阻塞的通道寫入操作 ch <- 2,由於通道已滿,操作會立即返回。我們使用 select 語句來處理這種情況,當無法進行通道寫入操作時,會執行 default 分支,輸出 "Channel is full, unable to write data"。

然後,我們嘗試從通道中讀取數據 data, ok := <-ch,由於通道中有數據,操作不會被阻塞。

最後,我們使用非阻塞的通道讀取操作 data, ok := <-ch,由於通道爲空,操作會立即返回。同樣,我們使用 select 語句來處理這種情況,當無法進行通道讀取操作時,會執行 default 分支,輸出 "Channel is empty, unable to read data"。

超時處理

通過在 select 語句中結合使用 time.After 函數和通道操作,可以實現超時機制。例如,可以使用 select 監聽一個帶有超時的通道操作,當超過指定時間時,執行相應的操作。

package main

import (
 "fmt"
 "time"
)

func main() {
 ch := make(chan int)

 go func() {
  time.Sleep(2 * time.Second)
  ch <- 1
 }()

 select {
 case <-ch:
  fmt.Println("Received from channel")
 case <-time.After(3 * time.Second):
  fmt.Println("Timeout")
 }
}

在這個示例中,我們創建了一個通道 ch。然後,我們在一個 goroutine 中進行操作,在 2 秒後向通道發送數據 ch <- 1

在 main 函數中,我們使用 select 語句同時監聽 ch 通道和 time.After 函數返回的超時通道。超時通道是一個計時器通道,在指定的時間後會發送一個值給通道。

在 select 語句的執行過程中,會依次檢查每個 case 子句。如果 ch 通道接收到了數據,就會執行 case <-ch 分支,輸出 "Received from channel"。如果等待時間超過了設定的超時時間(這裏是 3 秒),就會執行 time.After 對應的 case 分支,輸出 "Timeout"。

在這個示例中,由於通道的發送操作需要 2 秒才能完成,而超時時間設定爲 3 秒,所以最終會執行 case <-ch 分支,輸出 "Received from channel"。

控制併發流程

select 可以與 goroutine 結合使用,實現對併發流程的控制。通過在 select 中使用通道操作來進行同步或通信,可以協調不同 goroutine 之間的執行順序。

package main

import (
 "fmt"
 "sync"
)

func main() {
 var wg sync.WaitGroup

 // 設置併發任務數量
 concurrency := 3

 // 創建一個用於控制併發的通道
 semaphore := make(chan struct{}, concurrency)

 // 假設有一組任務需要併發執行
 tasks := []string{"task1""task2""task3""task4""task5"}

 // 遍歷任務列表
 for _, task := range tasks {
  // 增加 WaitGroup 的計數器
  wg.Add(1)

  // 啓動一個 goroutine 來執行任務
  go func(t string) {
   // 在 goroutine 開始前向通道發送一個信號
   semaphore <- struct{}{}

   // 執行任務
   fmt.Println("Executing", t)

   // 模擬任務執行時間
   // 這裏可以是任何實際的任務邏輯
   // ...

   // 任務完成後從通道釋放一個信號
   <-semaphore

   // 減少 WaitGroup 的計數器
   wg.Done()
  }(task)
 }

 // 等待所有任務完成
 wg.Wait()

 fmt.Println("All tasks completed")
}

在這個示例中,我們首先定義了併發任務的數量 concurrency,這決定了同時執行任務的最大數量。然後,我們創建了一個用於控制併發的通道 semaphore,通過向通道發送信號來控制併發數量。

接下來,我們定義了一組需要併發執行的任務列表 tasks。在遍歷任務列表時,我們增加了 WaitGroup 的計數器,並啓動一個 goroutine 來執行每個任務。

在每個任務的 goroutine 中,首先向通道 semaphore 發送一個信號,以佔用一個併發槽位。然後執行任務的邏輯,這裏使用了簡單的輸出來表示任務的執行。任務執行完畢後,從通道 semaphore 中釋放一個信號,以讓其他任務可以佔用併發槽位。最後,減少 WaitGroup 的計數器,表示任務完成。

最後,我們使用 WaitGroup 的 Wait 方法來等待所有任務完成,確保程序在所有任務執行完畢後再繼續執行。

總結

以下是 select 語句的一些特性:

  1. 如果沒有任何通道操作準備好,且沒有默認的 case 子句,那麼 select 語句會被阻塞,直到至少有一個通道操作準備好。

  2. 如果有多個 case 子句準備好,那麼會隨機選擇一個執行。不會有優先級或順序的保證。

  3. select 語句可以用於發送和接收操作,也可以混合使用。

  4. select 語句可以與 for 循環結合使用,以實現對多個通道的連續監控和處理。

select 機制是 Golang 中處理併發操作的重要工具之一,它能夠很好地處理多個通道操作,避免阻塞和死鎖的問題。


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/h1foTSEnSoKkKFH2PyRuqA