golang select底層原理

辉辉、發表於2024-03-16

select 語句在 Go 語言中用於在多個通訊操作(傳送和接收)之間進行選擇。在底層,Go 執行時維護了關於每個 case 的狀態資訊,這些資訊通常儲存在 scase 結構體中。下面我將詳細解釋 select 語句的工作原理,並嘗試用文字描述來模擬這一過程,因為直接在這裡畫圖可能不太方便。

首先,讓我們回顧一下 scase 結構體:

go
type scase struct {
c *hchan // 通訊的channel
elem unsafe.Pointer // 指向資料元素的指標
// ... 其他欄位,比如用於表示case型別的標記、用於goroutine排程的資訊等
}
在 Go 的併發模型中,hchan 是 channel 的內部表示,它包含了諸如鎖、等待佇列、元素佇列等狀態資訊。

當執行一個 select 語句時,Go 執行時會做以下幾步:

建立 scase 陣列:
執行時首先根據 select 語句中的 case 數量建立一個 scase 陣列。每個 scase 對應一個 case。

初始化 scase:
對於每個 case,執行時設定 scase 的 c 欄位為對應的 channel 的指標,並可能設定 elem 欄位為指向資料元素的指標(如果是接收操作)。

檢查可執行的 case:
執行時檢查每個 scase,確定是否有可以立即執行的 case。這通常涉及檢查 channel 的狀態,例如它是否已關閉、是否有待接收的資料或空間可供傳送資料。

執行可執行的 case:
如果找到可以立即執行的 case,則執行該 case 對應的操作(傳送或接收),並從 select 語句返回。

處理阻塞情況:
如果沒有找到可以立即執行的 case,但 select 語句包含 default 分支,則執行 default 分支。如果沒有 default 分支,則當前 goroutine 會被阻塞,直到至少有一個 case 變得可執行。此時,goroutine 會被放入 channel 的等待佇列中,等待其他 goroutine 的操作來喚醒它。

喚醒和排程:
當某個 channel 的狀態發生變化(例如有資料可讀或可寫)時,等待在該 channel 上的 goroutine 會被喚醒,並重新檢查 select 語句中的 case 是否現在可以執行。然後,執行時根據排程策略決定哪個 goroutine 繼續執行。

下面是一個簡化的流程圖,描述了這個過程:

+----------------+ +----------------+ +----------------+
| 開始執行select | --> | 建立scase陣列 | --> | 檢查可執行的case |
+----------------+ +----------------+ +----------------+
|
| No
v Yes
+----------------+ +----------------+
| 執行該case | | 阻塞或default |
+----------------+ +----------------+
|
| 狀態變化
v
+----------------+ +----------------+
| 喚醒goroutine| | 繼續執行其他 |
+----------------+ +----------------+
|
v
+----------------+
| 結束select |
+----------------+
需要注意的是,實際的 Go 執行時中 select 的實現要比這個簡化模型複雜得多,它涉及到更多的併發控制和排程策略。此外,為了最佳化效能,Go 執行時還使用了諸如輪詢(polling)和避免不必要的阻塞等技術。

希望這個解釋和流程圖能幫助你更好地理解 Go 語言中 select 語句的底層工作原理。如果你需要更深入的細節或具體的實現程式碼,建議查閱 Go 語言的原始碼,特別是與併發和排程相關的部分。

相關文章