写点什么

深入剖析 Go 语言中的 Channel:高级特性与注意事项

作者:Jack
  • 2023-04-19
    广东
  • 本文字数:2159 字

    阅读完需:约 7 分钟

深入剖析Go语言中的Channel:高级特性与注意事项

一、channel 的基本原理

chan 类型的数据结构如下图所示:

下面我们先来具体解释各个字段的含义:

  • qcount:代表 chan 中已经接收但还没被取走的元素的个数。内建函数 len 可以返回这个字段的值。dataqsiz:队列的大小。chan 使用一个循环队列来存放元素,循环队列很适合这种生产者 - 消费者的场景(我很好奇为什么这个字段省略 size 中的 e)。

  • buf:存放元素的循环队列的 buffer。elemtype 和 elemsize:chan 中元素的类型和 size。因为 chan 一旦声明,它的元素类型是固定的,即普通类型或者指针类型,所以元素大小也是固定的。

  • sendx:处理发送数据的指针在 buf 中的位置。一旦接收了新的数据,指针就会加上 elemsize,移向下一个位置。buf 的总大小是 elemsize 的整数倍,而且 buf 是一个循环列表。

  • recvx:处理接收请求时的指针在 buf 中的位置。一旦取出数据,此指针会移动到下一个位置。

  • recvq:chan 是多生产者多消费者的模式,如果消费者因为没有数据可读而被阻塞了,就会被加入到 recvq 队列中。

  • sendq:如果生产者因为 buf 满了而阻塞,会被加入到 sendq 队列中。

二、channel 的高级特性

1.双向 Channel 和单向 Channel

在使用 Channel 时,我们可以将其定义为双向 Channel 或者单向 Channel。双向 Channel 可以进行发送和接收操作,而单向 Channel 只能进行发送或者接收操作。

双向 Channel 的声明方式如下:

var ch chan int // 双向 Channel
复制代码

单向 Channel 的声明方式如下:

var sendCh chan<- int // 只能用于发送数据的 Channelvar recvCh <-chan int // 只能用于接收数据的 Channel
复制代码

2.带缓冲区的 Channel 和无缓冲区的 Channel

在创建 Channel 时,我们可以指定缓冲区的大小。如果缓冲区大小为 0,则代表该 Channel 没有缓冲区,也就是无缓冲区的 Channel。如果缓冲区大小大于 0,则代表该 Channel 是带缓冲区的 Channel。


带缓冲区的 Channel 可以在缓冲区未满时进行发送操作,而无缓冲区的 Channel 只能在接收操作完成前进行发送操作,否则会阻塞当前 goroutine。

3.Range 遍历 Channel

在 Go 语言中,我们可以使用 for range 语句来遍历 Channel,这种方式可以避免手动进行循环和判断 Channel 是否关闭的操作。

语法如下:

for data := range ch {    // 处理从 ch 中接收到的数据}
复制代码

当 Channel 被关闭后,for range 就会自动退出循环。

4.Channel 的选择器

在使用多个 Channel 进行数据交换时,我们可以使用 Channel 的选择器来实现非阻塞式的数据交换。选择器会等待多个 Channel 中的一个可用,并执行相应的操作,如果多个 Channel 同时可用,则随机选择一个进行操作。


选择器的语法如下:

select {case data, ok := <-ch1:    if ok {        // 处理从 ch1 接收到的数据    } else {        // ch1 已经关闭    }case data, ok := <-ch2:    if ok {        // 处理从 ch2 接收到的数据    } else {        // ch2 已经关闭    }case ch3 <- data:    // 向 ch3 发送数据 datadefault:    // 如果所有 Channel 都没有数据可接收或者发送,执行 default 分支}
复制代码

5.Channel 的超时机制

在使用 Channel 进行数据交换时,我们可以使用带有超时机制的操作来避免 goroutine 的长时间阻塞。在 Go 语言中,我们可以使用 time 包中的 Timer 和 Ticker 来实现超时机制。


Timer 用于等待一段时间后执行某个操作,而 Ticker 则用于每隔一段时间执行某个操作。

Timer 的语法如下:

timer := time.NewTimer(time.Second) // 创建一个 1 秒钟的 Timer<-timer.C // 等待 Timer 的时间到达
复制代码

Ticker 的语法如下:

for range ticker.C {    // 每秒钟执行一次的操作}
复制代码


三、使用 Channel 容易犯的错误


首先,我们来总结下会 panic 的情况,总共有 3 种:

  • close 为 nil 的 chan;

  • send 已经 close 的 chan;

  • close 已经 close 的 chan。

goroutine 泄漏的问题也很常见,下面的代码也是一个实际项目中的例子:

func process(timeout time.Duration) bool {    ch := make(chan bool)
go func() { // 模拟处理耗时的业务 time.Sleep((timeout + time.Second)) ch <- true // block fmt.Println("exit goroutine") }() select { case result := <-ch: return result case <-time.After(timeout): return false }}
复制代码


在这个例子中,process 函数会启动一个 goroutine,去处理需要长时间处理的业务,处理完之后,会发送 true 到 chan 中,目的是通知其它等待的 goroutine,可以继续处理了。


我们来看一下第 10 行到第 15 行,主 goroutine 接收到任务处理完成的通知,或者超时后就返回了。

如果发生超时,process 函数就返回了,这就会导致 unbuffered 的 chan 从来就没有被读取。我们知道,unbuffered chan 必须等 reader 和 writer 都准备好了才能交流,否则就会阻塞。


超时导致未读,结果就是子 goroutine 就阻塞在第 7 行永远结束不了,进而导致 goroutine 泄漏。解决这个 Bug 的办法很简单,就是将 unbuffered chan 改成容量为 1 的 chan,这样第 7 行就不会被阻塞了。


一般而言,有几个原则可以记住:

  • 共享资源的并发访问使用传统并发原语;

  • 复杂的任务编排和消息传递使用 Channel;

  • 消息通知机制使用 Channel,除非只想 signal 一个 goroutine,才使用 Cond;

  • 简单等待所有任务的完成用 WaitGroup

用户头像

Jack

关注

还未添加个人签名 2019-05-12 加入

作为一名技术追求者,我对科技、编程和创新充满热情。我始终关注最新的商业趋势和技术发展,努力将其应用于实践中,从而推动技术创新和改进。我善于思考和分析,具备较强的解决问题的能力和团队合作精神。

评论

发布
暂无评论
深入剖析Go语言中的Channel:高级特性与注意事项_Jack_InfoQ写作社区