golang - 阻塞场景
一、阻塞的核心场景
-
无缓冲通道(Unbuffered Channel)
-
发送操作会阻塞,直到另一个goroutine准备好接收。
-
接收操作会阻塞,直到另一个goroutine准备好发送。
-
-
带缓冲通道(Buffered Channel)
-
发送操作仅在缓冲区满时阻塞。
-
接收操作仅在缓冲区空时阻塞。
-
-
其他场景
-
time.Sleep
、http.Get
、文件读写等I/O操作。 -
sync.WaitGroup
、sync.Mutex
等同步工具。
-
二、代码示例
示例1:无缓冲通道的阻塞
package main import "fmt" func main() { ch := make(chan int) // 无缓冲通道 go func() { fmt.Println("准备发送数据: 42") ch <- 42 // 发送数据,会阻塞直到主goroutine接收 fmt.Println("数据已发送") }() fmt.Println("准备接收数据") fmt.Println("收到数据:", <-ch) // 接收数据,解除子goroutine的阻塞 // 等待用户输入,防止主goroutine退出 fmt.Scanln() }
输出:
准备接收数据 准备发送数据: 42 收到数据: 42 数据已发送
解释:
-
子goroutine在发送
ch <- 42
时阻塞,直到主goroutine执行<-ch
。 -
主goroutine的
fmt.Scanln()
是为了防止程序提前退出。
示例2:带缓冲通道的阻塞
package main import "fmt" func main() { ch := make(chan int, 2) // 缓冲区大小为2 ch <- 1 // 不阻塞(缓冲区未满) ch <- 2 // 不阻塞(缓冲区未满) fmt.Println("发送了1和2") go func() { ch <- 3 // 阻塞(缓冲区已满),直到主goroutine接收数据 fmt.Println("发送了3") }() fmt.Println("接收:", <-ch) // 接收1,腾出一个缓冲区位置 fmt.Println("接收:", <-ch) // 接收2 fmt.Println("接收:", <-ch) // 接收3,解除子goroutine的阻塞 }
输出:
发送了1和2 接收: 1 接收: 2 接收: 3 发送了3
示例3:用select
处理阻塞
package main import ( "fmt" "time" ) func main() { ch := make(chan int) quit := make(chan bool) go func() { for { select { case num := <-ch: fmt.Println("收到:", num) case <-quit: fmt.Println("退出goroutine") return default: fmt.Println("等待数据...") time.Sleep(500 * time.Millisecond) } } }() ch <- 1 ch <- 2 quit <- true time.Sleep(1 * time.Second) // 等待子goroutine退出 }
输出:
等待数据... 收到: 1 收到: 2 退出goroutine
解释:
-
select
的default
分支用于实现非阻塞操作。 -
如果没有
default
分支,select
会阻塞直到某个case就绪。
三、典型阻塞问题:死锁
如果所有goroutine都被阻塞,程序会触发死锁(panic):
func main() { ch := make(chan int) ch <- 42 // 发送阻塞(没有接收方) fmt.Println(<-ch) }
运行结果:
fatal error: all goroutines are asleep - deadlock!
四、总结
-
阻塞是Go并发模型的核心机制,通过通道的阻塞/非阻塞行为协调goroutine。
-
避免死锁:确保至少有一个goroutine能继续执行。
-
工具:使用
select
、带缓冲通道、sync.WaitGroup
等处理阻塞逻辑。