golang 面試題:Goroutinue 什麼時候會被掛起?
今天我們來聊聊一個在 Go 面試中經常遇到的經典問題:Goroutine 什麼時候會被掛起?
如果你是一個 Go 程序員,或者正在準備 Go 相關的面試,可能對這個問題有一些疑問。那麼,就讓我從一個資深程序員的角度,帶你們深入淺出地分析這個問題。💡
什麼是 Goroutine?
首先,咱們得搞清楚什麼是 Goroutine。簡而言之,Goroutine 就是 Go 中的輕量級線程。它讓我們可以用極低的成本來啓動併發執行的任務。創建一個 Goroutine 非常簡單,只需要一個 go 關鍵字,比如:
go func() {
fmt.Println("Hello from Goroutine!")
}()
這段代碼就是啓動了一個新的 Goroutine 來執行裏面的匿名函數。
Goroutine 的優勢在於它比傳統的線程更加輕量,這得益於 Go runtime 的調度器。Goroutine 可以在同一個線程上調度執行,多個 Goroutine 會共享內存中的堆棧空間。這讓我們可以在 Go 程序中啓動成千上萬的 Goroutine,而不會像傳統線程那樣消耗大量資源。
Goroutine 是如何調度的?
在談論 “掛起” 之前,我們必須瞭解 Goroutine 的調度機制。Go 的運行時會通過一個協作式調度器來管理這些 Goroutine。當一個 Goroutine 執行時,它會被稱爲“運行中的 Goroutine”。但是,Go 的調度器是協作式的,這意味着它不是基於時間片的,而是依賴於程序中發生的事件來切換 Goroutine。
Goroutine 什麼時候會被掛起?
好,話說到這裏,我們終於可以開始回答正題:Goroutine 什麼時候會被掛起?實際上,Goroutine 會在以下幾種情況下被掛起:
1. 等待同步操作
比如使用 sync.WaitGroup 或 channel 來等待其他 Goroutine 完成時。舉個例子,假設你有多個 Goroutine 在執行某些任務,而你希望主 Goroutine 等待它們完成後再繼續執行:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Printf("Goroutine %d is done\n", i)
}(i)
}
wg.Wait() // 主 Goroutine 會等待所有 Goroutine 完成
fmt.Println("All Goroutines are done.")
在這個例子中,主 Goroutine 會調用 wg.Wait(),這時它會掛起,直到所有 Goroutine 都調用了 wg.Done(),纔會繼續執行。
2. 阻塞在 Channel 上
Goroutine 會在 channel 的發送或接收操作上被掛起,直到對方做出響應。舉個簡單的例子:
ch := make(chan int)
go func() {
fmt.Println("Sending data to channel")
ch <- 1 // 發送數據
}()
fmt.Println("Receiving data from channel")
data := <-ch // 接收數據,主 Goroutine 會在這裏掛起
fmt.Println("Received:", data)
在這個例子中,主 Goroutine 在接收數據時會被掛起,直到另一個 Goroutine 向 ch 通道發送數據。
如果沒有其他 Goroutine 來寫數據,接收操作會一直掛起,直到有數據可接收。反過來,如果主 Goroutine 在發送數據,而沒有其他 Goroutine 去接收它,發送操作也會掛起。
3. 執行阻塞操作
如果在 Goroutine 內執行了阻塞操作(如 I/O 操作、網絡請求等),Goroutine 會被掛起,直到操作完成。例如:
go func() {
fmt.Println("Simulating a network request...")
time.Sleep(2 * time.Second) // 模擬阻塞操作
fmt.Println("Network request complete!")
}()
這時,Goroutine 會在 time.Sleep 或其他阻塞操作上掛起,直到超時或操作完成。
4. 調用 runtime.Gosched()
runtime.Gosched() 是 Go 運行時提供的一個函數,調用它會強制當前 Goroutine 放棄 CPU 時間片,允許其他 Goroutine 執行。它不會掛起當前 Goroutine,但會讓當前 Goroutine 被調度器掛起,暫時讓出 CPU 資源。
go func() {
fmt.Println("Starting Goroutine")
runtime.Gosched() // 主動讓出 CPU 時間片
fmt.Println("Resumed Goroutine")
}()
這段代碼的執行順序會先輸出 "Starting Goroutine",然後當前 Goroutine 會被掛起,調度器會讓其他 Goroutine 執行,等到它被重新調度時,纔會繼續執行 "Resumed Goroutine"。
5. 阻塞的 select 語句
如果你使用 select 語句,且沒有合適的 case 被觸發,當前 Goroutine 也會被掛起。例如:
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
default:
fmt.Println("No messages received")
}
}()
time.Sleep(1 * time.Second)
如果 ch1 和 ch2 都沒有數據可接收,當前 Goroutine 會掛起,直到其中一個通道準備好數據。
6. 死鎖(Deadlock)
最後,Goroutine 會在發生死鎖時被掛起。例如,在多個 Goroutine 間相互等待對方完成時,程序會出現死鎖。Go 的運行時會檢測死鎖,並報告錯誤。
理解 Goroutine 掛起的場景,不僅有助於你理解 Go 的併發模型,還能讓你在開發過程中避免不必要的性能問題。比如,你可能會遇到這種情況:啓動了很多 Goroutine,但它們卻因爲等待某些資源而陷入了掛起的狀態,造成性能瓶頸。這時就得看看是同步等待問題、通道阻塞問題,還是其他的死鎖問題。
作爲程序員,我們應該熟悉這些調度的細節,並在編寫併發程序時更加小心。避免不必要的掛起,讓你的程序更高效、更快速。
結語
總結一下,Goroutine 可能會在以下幾種情況下被掛起:等待同步、阻塞在通道操作、執行阻塞操作、調用 runtime.Gosched()、死鎖,或者在 select 語句中沒有匹配的 case。這些情況涉及到 Go 的調度機制、併發編程的特性,也是 Go 語言的一大亮點。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/r7LwuwILEc017cIQsbAYhA