Channel【Golang】

来自智得网
(重定向自Channel(Golang)
跳转至: 导航、​ 搜索

简介

“不要通过共享内存来通信,通过通信来共享内存”,——rob pike
Golang的CSP通信模式

对于程序而言,不同线程/进程之间的数据共享模式大致可以分为两类

  • 通过共享内存的方式实现,本质上就是多线程模式,大多数面向过程或者面向对象的语言一般都采用该方式,共享内存即不同线程访问同一个变量,每个线程可以看到其他线程对变量的变更,共享内存方式存在以下缺点:
    • 变量的更新操作需要同步机制,在性能方面,实现复杂度方面较高。
    • 变量的更新需要对其他线程立即可见,需要额外的保障机制。
    • 变量变更的追踪机制复杂,业务逻辑较为复杂。
  • 通过通信来共享数据,通用的模式有Actor和CSP两种模式,Actor模型中,主角是Actor,Actor一般是协程机制的worker,Actor彼此之间直接发送消息,不需要经过间介质,消息是异步发送和处理的,CSP机制的worker没有Actor自带的信箱机制,需要通过管道(Channel)通信,通过channel,CSP机制的worker之间耦合性更低。
    • Channel的通信模式,数据通过消息进行交互,数据发送之后不再和其他线程共享,不需要复杂的同步机制。
    • Channel天然实现了异步通信机制,性能更高。

Golang语言中的Channel是协程(Goroutine)之间通信的管道,用于协程之间通讯和同步。

要素

要素 特点
空间 通道需要通过make初始化,分配空间才可以使用。

没有初始化的通道会被置为nil,无法读写数据。

流向 Channel使用箭头方向代表数据流向,c<-5,代表5写入Channel c,<-c代表读取Channel c

同理声明Channel,除了可以声明双向Channel,chan T,还可以声明单向通道,chan<-T(可写通道),<-chan T(可读通道)

缓冲 Channel可以在make的时候确定Channel是否有有缓冲。

make(chan int, 3)表示创建一个缓冲区的Channel,如果第二个Size参数为空,则代表Channel无缓冲。

无缓冲通道的读写必须由不同协程完成,否则会产生死锁。通道的读取操作是阻塞类型的,如果读取不到数据会一直等待。

写入操作如果在缓冲区已满或者无缓冲通道没有读取操作(也可以视为缓冲已满)的情况下会阻塞。

阻塞性的Chanel读写要求每个Channel的读写操作都提供单独的协程,和IO多路复用的原理类似,Channel也提供了Select+Switch的操作。

一个协程可以监听多个Channel的读取操作,只要任意一个Channel有数据可以读取,Select可以完成正确的调度。

如果所有Channel都未进入可读取状态,Select所在的协程进入阻塞状态。

状态 close函数可以关闭一个通道,但是关闭的通道不能写入数据,但是依然可以读取到一个0的值。在循环读取的时候一定要住要,避免陷入无尽的循环。

data, ok<-chan,读取的第二个参数为false可以判断channel已经关闭。

原理

Channel结构体

Channel用于交换Goroutine之间的数据,缓冲模式的Channel是异步模式的,所以需要有数据的存储组件,该存储组件是采用环形队列实现的。

对Channel的写操作可能因为存储空间已满的原因发生阻塞,同理读操作可能会因为数据为空而发生阻塞,需要保存这些阻塞的Goroutine,Channel使用双向链表分别保存读写发生阻塞的Goroutine。

Channel的写流程如下:

  • 锁定Channel。
  • 首先尝试从等待队列中获取Goroutine,然后将元素直接写入Goroutine。
  • 如果等待队列为空,则确定缓冲区是否可用。如果可用,从当前goroutine复制数据到缓冲区。
  • 如果缓冲区已满,则要写入的元素将保存在当前正在执行的goroutine的结构中,并且当前goroutine将在发送队列中排队并从运行时挂起。
  • 写入完成释放锁。

Channel的读流程和写流程基本一致。


Goroutine创建之后返回的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
 ...
}