Go协程死锁问题:基于select模式的排查与优化方案
问题分析
这段Go代码触发死锁的核心原因有两个:
goOne和goTwo向无缓冲通道发送数据,但main goroutine里的select执行太快,还没等子goroutine把数据发进通道就走到了default分支,没有完成接收操作。- 无缓冲通道的发送操作会一直阻塞,直到有对应的接收操作完成。两个子goroutine卡在
ch1 <- "Channel 1"和ch2 <- "Channel 2"处,无法执行wg.Done(),导致main goroutine的wg.Wait()永远等待,最终所有goroutine都进入休眠状态,触发死锁。
另外原代码的select分支还有逻辑错误:比如case <-ch1:已经接收了一次通道值,后面又调用fmt.Println(<-ch1)会再次尝试接收,这会导致额外阻塞(通道里已无数据)。
原代码示例
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup ch1 := make(chan string) ch2 := make(chan string) wg.Add(2) go goOne(&wg, ch1) go goTwo(&wg, ch2) select { case <-ch1: fmt.Println(<-ch1) close(ch1) case <-ch2: fmt.Println(<-ch2) close(ch2) default: fmt.Println("Default Case") } wg.Wait() } func goTwo(wg *sync.WaitGroup, ch2 chan string) { ch2 <- "Channel 2" wg.Done() } func goOne(wg *sync.WaitGroup, ch1 chan string) { ch1 <- "Channel 1" wg.Done() }
原代码输出
Default Case fatal error: all goroutines are asleep - deadlock! goroutine 1 [semacquire]: sync.runtime_Semacquire(0xc000108270?) /usr/local/go/src/runtime/sema.go:62 +0x25 sync.(*WaitGroup).Wait(0x4b9778?) /usr/local/go/src/sync/waitgroup.go:139 +0x52 main.main() /home/nidhey/Documents/Go_Learning/goroutines/select.go:29 +0x2af goroutine 6 [chan send]: main.goOne(0x0?, 0x0?) /home/nidhey/Documents/Go_Learning/goroutines/select.go:39 +0x28 created by main.main /home/nidhey/Documents/Go_Learning/goroutines/select.go:14 +0xc5 goroutine 7 [chan send]: main.goTwo(0x0?, 0x0?) /home/nidhey/Documents/Go_Learning/goroutines/select.go:33 +0x28 created by main.main /home/nidhey/Documents/Go_Learning/goroutines/select.go:15 +0x119
修复方案
基于select模式处理通道阻塞,提供三种可行修复方式:
方式一:select循环接收所有通道数据
通过循环select接收两个通道的数据,结合WaitGroup等待子goroutine完成,确保所有数据都被处理:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup ch1 := make(chan string) ch2 := make(chan string) wg.Add(2) go goOne(&wg, ch1) go goTwo(&wg, ch2) // 用done通道标记所有goroutine完成 done := make(chan struct{}) go func() { wg.Wait() close(done) close(ch1) close(ch2) }() // 循环select接收通道数据,直到所有任务完成 for { select { case msg, ok := <-ch1: if ok { fmt.Println(msg) } case msg, ok := <-ch2: if ok { fmt.Println(msg) } case <-done: return } } } func goTwo(wg *sync.WaitGroup, ch2 chan string) { ch2 <- "Channel 2" wg.Done() } func goOne(wg *sync.WaitGroup, ch1 chan string) { ch1 <- "Channel 1" wg.Done() }
方式二:去掉default分支,让select等待通道数据
原代码的default分支是提前跳过接收的关键,去掉后select会阻塞直到某个通道有数据可读,同时修正重复接收的问题:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup ch1 := make(chan string) ch2 := make(chan string) wg.Add(2) go goOne(&wg, ch1) go goTwo(&wg, ch2) // 等待第一个到达的通道数据并打印 select { case msg := <-ch1: fmt.Println(msg) case msg := <-ch2: fmt.Println(msg) } // 启动goroutine接收剩余通道数据,避免子goroutine阻塞 go func() { <-ch1 }() go func() { <-ch2 }() wg.Wait() } func goTwo(wg *sync.WaitGroup, ch2 chan string) { ch2 <- "Channel 2" wg.Done() } func goOne(wg *sync.WaitGroup, ch1 chan string) { ch1 <- "Channel 1" wg.Done() }
方式三:使用缓冲通道
如果不需要严格同步,可将通道改为带缓冲的,发送操作不会立即阻塞:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup // 创建带1个缓冲的通道 ch1 := make(chan string, 1) ch2 := make(chan string, 1) wg.Add(2) go goOne(&wg, ch1) go goTwo(&wg, ch2) select { case msg := <-ch1: fmt.Println(msg) case msg := <-ch2: fmt.Println(msg) default: fmt.Println("Default Case") } wg.Wait() // 接收剩余数据,避免资源泄漏 <-ch1 <-ch2 } func goTwo(wg *sync.WaitGroup, ch2 chan string) { ch2 <- "Channel 2" wg.Done() } func goOne(wg *sync.WaitGroup, ch1 chan string) { ch1 <- "Channel 1" wg.Done() }
内容的提问来源于stack exchange,提问作者Nidhey Indurkar
相关产品推荐
相关产品推荐

