channel

channel

简介

channel是goroutine之间通信的重要桥梁,借助channel,能够轻易的写出一个多协程通信程序。

概念

channel是一个通道,用于端到端的数据传输,跟消息队列有点像,只不过channel的发送和接收方都是goroutine对象,属于内存级别的通信。

传统的线程通信方式有很多,共享内存、信号量等,其中共享内存实现较为简单,只需要对变量进行并发控制,加锁即可,但这种在后续业务逐渐复杂时,将很难维护,耦合性也比较强。

后来提出了CSP模型,即在通信双方抽象出中间层,数据的流转在中间层来控制,通信双方只负责数据的发送和接收,从而实现了数据共享,这就是所谓的通过通信来共享内存,channel就是按照这个模型来实现的。

channel在并发操作里,是属于协程安全的。并且遵循了FIFO特性,则先执行读取的goroutine会先获取数据,先发送数据的goroutine会先输入数据。

另外,channel的使用将会引起goroutine的调度调用,会有阻塞和唤起goroutine的情况产生[gopark/goready]。

channel的创建

无缓冲的channel,一旦有goroutine在channel发送数据,那么当前的goroutine会被阻塞住,直到有其他的goroutine消费了channel里面的数据,才能继续运行

ch := make(chan int)

有缓冲的channel,第二个参数表示channel可缓冲的容量,只要当前channel里面的元素总数不大于这个可缓冲容量,则当前的goroutine就不会被阻塞

ch := make(chan int, 2)

分类

  • 无缓冲

强同步,发送/接收必须同时就绪[同时进行]。

  • 有缓冲

解耦生产、消费速度,避免瞬间阻塞。

状态

  • 关闭
  • 发送
  • 接收

channel的读写

  • 写操作
ch := make(chan int)
ch <- 1
  • 读操作
data <- ch
  • 关闭
close(ch)

注意项

  • 当channel被关闭后,如果继续往里面写数据,则会直接panic退出;
  • 关闭channel后,还可以读取数据;
  • 如果关闭后的channel没有数据可读,将得到零值,即对应类型的默认值;
  • 关闭原则
    • 发送方关闭,接收方用for rang读取
    • 不要从接收方关闭channel
判断channel是否被关闭
if v,ok := <-ch; !ok {
    fmt.Println("channel已关闭")
}

channel和select

在写程序时,有时并不单单只会和一个goroutine通信,当需要进行多goroutine通信时,则会使用select写法来管理多个channel的通信数据。

channel和deadlock

往channel里读写数据时,有可能被阻塞住,一旦被阻塞,则需要其他goroutine执行对应的读写操作,才能解除阻塞状态;

然而,阻塞后一直没有发生调度行为,没有可用的goroutine可执行,则会一直卡在这个地方,程序就失去了执行意义,此时,go就会报deadlock错误;

因此,在使用channel时,要注意goroutine的一发一取,避免goroutine永久阻塞。

image

channel的底层原理

channel创建后,返回hchan结构体,主要字段如下:

type hchan struct {
 qcount   uint   // channel 里的元素计数
 dataqsiz uint   // 可以缓冲的数量,如 ch := make(chan int, 10)。 此处的 10 即 dataqsiz
 elemsize uint16 // 要发送或接收的数据类型大小
 buf      unsafe.Pointer // 当 channel 设置了缓冲数量时,该 buf 指向一个存储缓冲数据的区域,该区域是一个循环队列的数据结构
 closed   uint32 // 关闭状态
 sendx    uint  // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已发送数据的索引位置
 recvx    uint  // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已接收数据的索引位置
 recvq    waitq // 想读取数据但又被阻塞住的 goroutine 队列
 sendq    waitq // 想发送数据但又被阻塞住的 goroutine 队列

 lock mutex
 ...
}

channel在进行读写数据时,会根据有无缓冲设置对应的阻塞唤起动作,他们之间是有区别的。

无缓冲channel

由于对channel的读写先后顺序不同,处理也有有所不同,需要进一步区分。

channel先写后读

暂认为有2个goroutine在使用channel通信,按先写再读的顺序,具体流程如下:

image

可以看到,由于channel是无缓冲的,所以G1暂时被挂在sendq队列里,然后G1调用了gopark休眠了起来;

接着,又有goroutine来channel读取数据了:

image

此时,G2发现sendq等待队列里有goroutine存在,于是直接从G1copy数据过来,并且会对G1设置goready函数,这样下次发生调度时,G1就可以继续运行,并且会从等待队列里移除掉。

channel先读再写

image

G1暂时被挂在recvq队列,然后休眠起来

G2在写数据时,发现recvq队列有goroutine存在,于是直接将数据发送给G1,同时设置G1 goready函数,等待下次调度运行

image

有缓冲channel

跟无缓冲channel一样,分两种情况

channel先写后读

这次会优先判断缓冲数据区域是否已满,如果未满,将数据保存在缓冲数据区域,即环形队列里,如果已满,则和之前的流程一致[放到sendq等待队列]。

image

当G2要读取数据时,会优先从缓冲数据区域去读取,并且在读取完之后,会检查sendq队列,如果goroutine有等待队列,则会将它上面的data补充到缓冲数据区域,并且也对其设置goready函数。

image

channel先读再写

此情况和无缓冲的先读再写一个流程

总结

有无缓冲channel的读写基本相差不大,只是多了缓冲数据区域的判断。

channel在使用的时候大多都和select配合使用,尽管只需要简单的用<-ch和ch<-来读写数据,但是它的底层还是有讲究的,特别是涉及到调度的休眠唤起。

posted @ 2025-08-08 22:19  biby  阅读(123)  评论(0)    收藏  举报