Golang 由兩個 chan 組成的鎖死
這是有一次在工作中發生的情況,當時 debug 很久。原來不是只有 Mutex 才會發生鎖死。
以下是一個涉及兩個 channel 的死鎖示例:
雙 Channel 死鎖示例
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
v := <-ch1 // 等待從 ch1 接收數據
ch2 <- v // 將接收到的數據發送到 ch2
}()
ch1 <- <-ch2 // 主 goroutine 嘗試從 ch2 接收並發送到 ch1
}
這個程序會產生死鎖,原因如下:
- 主 goroutine 創建了兩個無緩衝的 channel:
ch1
和ch2
。 - 一個新的 goroutine 被創建,它首先嘗試從
ch1
接收數據,然後將接收到的數據發送到ch2
。 - 主 goroutine 嘗試從
ch2
接收數據,然後將接收到的數據發送到ch1
。 - 兩個 goroutine 都被阻塞,等待對方的操作完成,但由於互相依賴,它們永遠無法完成操作。
死鎖的原因
這種情況下發生死鎖的根本原因是:
- 兩個 goroutine 都在等待對方完成操作。
- 使用了無緩衝的 channel,導致每個發送操作都需要有相應的接收操作同時進行。
- 操作順序的相互依賴形成了一個循環等待的情況。
如何避免
要避免這種死鎖,可以採取以下方法:
- 使用帶緩衝的 channel:
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
- 改變操作順序,打破循環依賴:
go func() {
ch2 <- 1 // 首先向 ch2 發送數據
v := <-ch1 // 然後從 ch1 接收數據
}()
v := <-ch2 // 主 goroutine 首先從 ch2 接收數據
ch1 <- v // 然後向 ch1 發送數據
- 使用 select 語句來同時處理多個 channel:
select {
case v1 := <-ch1:
ch2 <- v1
case v2 := <-ch2:
ch1 <- v2
default:
// 避免阻塞
}
- 使用 context 或 done channel 來實現超時或取消機制:
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
select {
case v := <-ch1:
ch2 <- v
case <-ctx.Done():
fmt.Println("Operation timed out")
}
通過這些方法,我們可以有效地避免涉及多個 channel 的死鎖情況,提高程序的健壯性和可靠性。