跳到主要内容

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:produceconsumeproduce 函数每秒向通道发送一个整数,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,应避免这种情况。