Go 通道(Channel)阻塞详解
利用 chan
造成阻塞
package main
import (
"fmt"
"sync"
"time"
)
var numbers []int
var wg sync.WaitGroup
var ch = make(chan int)
func produce() {
defer wg.Done()
step := 1
for {
ch <- step
step++
time.Sleep(1 * time.Second)
}
}
func consume() {
defer wg.Done()
for {
select {
case value := <-ch:
numbers = append(numbers, value)
fmt.Println("add", numbers)
time.Sleep(2 * time.Second)
}
}
}
func main() {
wg.Add(2)
go produce()
go consume()
wg.Wait()
}
在上述代码中,我创建了一个通道 ch
,并启动了两个 goroutine:produce
和 consume
。produce
函数每秒向通道发送一个整数,consume
函数从通道接收数据并将其添加到切片 numbers
中。由于通道的阻塞特性,生产者会在通道满时阻塞,消费者会在通道空时阻塞,实现了生产者和消费者的同步。
chan
的作用
同步机制
通道用于不同 goroutine 之间的同步操作。当一个 goroutine 向通道发送数据时,发送操作会阻塞,直到另一个 goroutine 从通道接收数据,实现了同步机制。
数据交换
通道可以在 goroutine 之间传递数据,充当数据交换的桥梁。当一个 goroutine 发送数据到通道时,另一个 goroutine 可以从通道接收这些数据。
阻塞操作
通道的发送和接收操作默认是阻塞的。当从一个空通道接收数据时,会阻塞等待数据到来;当向一个满的通道发送数据时,会阻塞等待通道有空闲空间。
缓冲与非缓冲
通道可以是无缓冲的(默认)或有缓冲的。无缓冲通道保证发送和接收同时发生,有缓冲通道允许发送者和接收者异步操作。
类型安全
通道是类型化的。一个 chan int
类型的通道只能传递 int
类型的数据,确保了类型安全。
协程间的通信
通道提供了一种方式,让一个 goroutine 可以等待另一个 goroutine,通过关闭通道或发送特定的信号来实现通信。
chan
的数据结构
type hchan struct {
qcount uint // 队列中的元素数量
dataqsiz uint // 循环队列的大小
buf unsafe.Pointer // 指向循环队列的指针
elemsize uint16 // 每个元素的大小
closed uint32 // 通道是否已关闭
elemtype *_type // 元素的类型
sendx uint // 发送索引
recvx uint // 接收索引
recvq waitq // 等待接收的 goroutine 队列
sendq waitq // 等待发送的 goroutine 队列
lock mutex // 互斥锁,保护通道
}
hchan
是 Go 语言中通道的底层实现结构,包含了通道的状态信息和等待队列。
工作流程
当一个 goroutine 试图向通道发送数据时:
- 检查
recvq
是否有等待接收的 goroutine,如果有,直接将数据传递给接收方。 - 如果
recvq
为空,检查缓冲区是否已满,未满则将数据加入缓冲区。 - 如果缓冲区已满,发送操作的 goroutine 会被阻塞,加入
sendq
等待。
接收数据的流程类似,但方向相反。
通道阻塞的场景
无缓冲的通道
当向无缓冲的通道发送数据时,发送操作会阻塞,直到有 goroutine 准备接收数据。接收操作同样会阻塞,直到有数据可接收。
有缓冲的通道
当向已满的有缓冲通道发送数据时,发送操作会阻塞,直到通道有空闲空间。接收操作从空的有缓冲通道接收数据时,也会阻塞,直到有新数据发送进来。
关闭的通道
向已关闭的通道发送数据会导致 panic。从已关闭的通道接收数据不会阻塞,会立即返回该类型的零值。
select
语句中的通道
使用 select
语句时,如果所有的 case
分支都阻塞,select
会等待直到某个分支可以执行。如果包含 default
分支,当所有 case
阻塞时,select
会执行 default
分支而不阻塞。
多个 goroutine 等待通道
如果有多个 goroutine 等待在同一个无缓冲通道上发送或接收数据,通道只会与其中一个 goroutine 配对,其他的继续阻塞。
何时使用关闭通道
发送者不再发送更多数据
当发送者确定不再发送数据时,应关闭通道,通知接收者数据已发送完毕。
ch := make(chan int)
// ... 发送数据 ...
close(ch) // 关闭通道
范围循环接收
接收者可以使用 for range
循环从通道接收数据,直到通道被关闭并且所有数据都被接收。
for val := range ch {
fmt.Println(val)
}
使用关闭作为信号
关闭通道可以作为信号,通知其他 goroutine 停止操作或退出。
如何正确关闭通道
只有发送者应关闭通道
只有发送数据的 goroutine 才应关闭通道,避免接收者关闭通道导致的 panic。
避免重复关闭通道
重复关闭通道会导致 panic,应确保通道只被关闭一次。
关闭后不能再发送数据
一旦通道被关闭,不能再向其中发送数据,任何发送操作都会导致 panic。
关闭后可继续接收数据
通道关闭后,仍可从中接收已发送的数据。接收操作会继续,直到通道中没有数据。
检查通道是否关闭
接收数据时,可以使用如下方式判断通道是否被关闭:
value, ok := <-ch
if !ok {
// 通道已关闭
}
关闭前的同步
在关闭通道前,确保所有数据都已发送完毕,可以使用 sync.WaitGroup
等同步机制。
使用关闭发送信号
关闭通道可作为发送信号,通知接收者或其他 goroutine 进行相应的处理。
注意事项
-
当所有的
select
分支都阻塞时,包含default
分支的select
会执行default
分支,不会阻塞。 -
通道被关闭后,仍然可以接收数据,直到数据被取完。
-
向已关闭的通道发送数据会导致 panic,应避免这种情况。