跳到主要内容

Go 通道为什么分为阻塞和非阻塞

· 阅读需 5 分钟
素明诚
Full stack development

通道(channel)的阻塞和非阻塞行为是 Go 语言并发编程模型的重要组成部分。这两种行为可以帮助程序员更灵活地控制 goroutine 之间的交互和同步。下面分别解释了阻塞和非阻塞通道的机制及其用途

阻塞通道(Blocking Channels)

  • 默认情况下,通道是阻塞的。当一个 goroutine 尝试从一个空的通道中读取数据时,它会被阻塞,直到另一个 goroutine 向通道写入数据为止。同样地,当一个 goroutine 尝试向一个已满的通道写入数据时(在带缓冲的通道的情况下),它会被阻塞,直到另一个 goroutine 从通道读取数据为止。
  • 这种阻塞机制提供了一种自然而强大的方式来同步并发操作。例如,它可以用于等待一个长时间运行的操作完成,或者协调多个 goroutine 的执行顺序。

非阻塞通道(Non-blocking Channels)

  • 非阻塞通道操作允许 goroutine 在尝试读取或写入通道时不被阻塞。在 Go 中,可以通过使用 select 语句和 default 子句来实现非阻塞通道操作。
  • 这种非阻塞机制可以用于实现更复杂的并发逻辑,例如超时、尝试操作或多路复用。它允许 goroutine 在通道操作无法立即进行时继续执行其他任务。
ch := make(chan int)

// 非阻塞读取示例
select {
case value := <-ch:
fmt.Println("Received value:", value)
default:
fmt.Println("No value received")
}

// 非阻塞写入示例
select {
case ch <- 1:
fmt.Println("Sent value")
default:
fmt.Println("No value sent")
}

select 语句尝试从 ch 通道读取值或向 ch 通道写入值。如果通道操作可以立即进行,则 select 会随机进入执行相应的 case 子句。如果通道操作无法立即进行,则 select 会执行 default 子句,实现非阻塞操作。

为什么要设计有缓冲区和非缓冲区呢?

答案就在同步和异步

无缓冲区通道(Unbuffered Channels)

  • 同步通信 无缓冲区通道提供了一种强同步的通信机制。当一个协程尝试向通道发送数据时,它会被阻塞,直到另一个协程从通道接收数据。同样,当一个协程尝试从通道接收数据时,它会被阻塞,直到另一个协程向通道发送数据。
  • 确保数据接收 通过无缓冲区通道发送数据时,发送协程会等待直到数据被接收,这确保了数据确实被接收并处理。
  • 简单和清晰 无缓冲区通道简化了协程间的通信,使得代码易于理解和维护。

有缓冲区通道(Buffered Channels)

  • 异步通信 有缓冲区通道允许异步的通信。协程可以向通道发送数据而不等待接收,只要通道的缓冲区还有空间。同样,协程可以从通道接收数据,只要缓冲区中有数据。
  • 提高性能 有缓冲区通道可以帮助提高程序的性能,因为协程可以继续执行,而不是等待其他协程。这对于需要高吞吐量或低延迟的应用程序特别有用。
  • 流量控制 有缓冲区通道的大小可以用作一种流量控制机制,以防止快速发送者溢出慢速接收者。

阻塞通道的使用场景

场景: 数据处理管道

在一个数据处理管道中,一个协程生成数据,另一个协程处理数据。阻塞通道确保生产者和消费者能够同步执行,避免生产者过快地产生数据,导致内存溢出。

package main

import (
"fmt"
"time"
)

func producer(ch chan int) {
for i := 0; i < 10; i++ {
fmt.Println("Produced:", i)
ch <- i // 此处会阻塞,直到消费者准备好接收
}
close(ch)
}

func consumer(ch chan int) {
for value := range ch {
fmt.Println("Consumed:", value)
time.Sleep(time.Millisecond * 100) // 模拟处理时间
}
}

func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}

非阻塞通道的使用场景及示例

场景: 事件通知

在一个事件驱动的程序中,可能需要一个非阻塞的方式来检查是否有新事件,同时继续执行其他任务。

package main

import (
"fmt"
"time"
)

func eventProducer(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
time.Sleep(time.Millisecond * 50) // 模拟事件产生的间隔
}
close(ch)
}

func main() {
ch := make(chan int, 10) // 创建带缓冲的通道
go eventProducer(ch)

for {
select {
case event, ok := <-ch:
if !ok {
fmt.Println("Channel is closed, no more events.")
return
}
fmt.Println("Received event:", event)
default:
fmt.Println("No new event, doing other work.")
time.Sleep(time.Millisecond * 100) // 模拟其他工作
}
}
}

在上面的示例中,select 语句的 default 分支提供了一种非阻塞的方式来检查通道,同时允许程序在没有新事件时执行其他任务。