Golang 对 channel 进行 for-range 会导致死锁吗
原创大约 3 分钟...
1. 死锁简短示例
// 这种情况很好理解, 出现死锁了
func main() {
ch := make(chan int, 1)
<-ch
}
2. 改用 for-range
// 这种情况也好理解, 其实和 1 完全一样的, 依旧死锁
func main() {
ch := make(chan int, 1)
for v := range ch {
fmt.Println(v)
}
}
3. 遍历直到死锁
// 这里就是容易产生误导的情况:
// 会误导我们以为 forrange 会导致死锁
func main() {
ch := make(chan int, 1)
go func() {
for i := 0; i < 3; i++ {
ch <- i
time.Sleep(time.Second * 1)
}
}()
for v := range ch {
fmt.Println(v)
}
}
// 其实这种情况可以等同于在 3 秒后退化成类似下面的代码, 有没有熟悉感, 这不就是情况 2 的代码
func main() {
ch := make(chan int, 1)
/* go func() {
for i := 0; i < 3; i++ {
ch <- i
time.Sleep(time.Second * 1)
}
}() */
for v := range ch {
fmt.Println(v)
}
}
// 可能这里还是会有疑问, 觉得结果不还是死锁吗?所以就可以得出结论:forrange 会遍历chan的值直到遍历完了产生死锁?
//
// 事实真的如此吗?真的是 forrange 本身导致的死锁吗?继续往下看
4. 增加代码验证死锁是不是真的由 for-range 造成的
// 这种情况下死锁没有出现
func main() {
ch := make(chan int, 1)
// 向 ch 写入
go func() {
for i := 0; i < 3; i++ {
ch <- i
time.Sleep(time.Second * 2)
}
// close(ch) // 这里是否 close 可以分别测一下, 其实都不影响结果, 区别是下面 forrange 是否会跳出循环
fmt.Println("closed")
}()
// forrange 读取
go func() {
for v := range ch {
fmt.Printf("v from chan: %v\n", v)
}
fmt.Println("读取结束了")
}()
// 这里增加了第三个 goroutine, 于是想象中的死锁没了
go func() {
for {
<-time.After(time.Second * 3)
fmt.Println("当前 goroutine 个数", runtime.NumGoroutine())
}
}()
select {}
}
5. 在 4 的基础上再做微调, 更神奇的事情发生了
// 这里在 4 的基础上去掉了向 ch 写入数据的 goroutine, 依然没有出现死锁
func main() {
ch := make(chan int, 1)
// 这里去掉了向 ch 写入的 goroutine
// forrange 读取
// 由于去掉了上面向 ch 写入的 goroutine, 这里永远的阻塞了
go func() {
for v := range ch {
fmt.Printf("v from chan: %v\n", v)
}
fmt.Println("读取结束了")
}()
// 这里增加了第三个 goroutine, 于是想象中的死锁没了
go func() {
for {
<-time.After(time.Second * 3)
fmt.Println("当前 goroutine 个数", runtime.NumGoroutine())
}
}()
select {}
}