2024-08-30
Go & 后端
00

目录

1 数据结构
2 使用方法
3 读写原理
4 其他信息

对Channel的使用方法和底层机制进行记录。

1 数据结构

channel 的内部实现是在src/runtime/chan.go文件里的 hchan 结构体。很多别的 builtin 函数,结构的源代码也在这个位置。

image.png

总体上来说,channel 是利用了一个环形数组来以队列的形式存储管道里的数据(之后所说的环形数组、队列和缓冲区,所指的就是这个数据结构),并且利用两个队列来存储等待中的待读协程和待写协程。

一个 channel,其内部有如下数据:

go
type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. // // Do not change another G's status while holding this lock // (in particular, do not ready a G), as this can deadlock // with stack shrinking. lock mutex }
  • qcount,uint,当前队列中剩余的元素个数。
  • dataqsiz,uint,环形数组长度,即总共可以存放的元素个数(make时传入的那个长度)。
  • buf,unsafe.Pointer,指向环形数组的那个指针。
  • elemsize,uint16,环形数组中每个元素的大小(或者说创建channel时指定的类型的实例的大小)。
  • closed,uint32,用于标识channel是否关闭。
  • elemtype,*_type,元素类型。
  • sendx,uint,队列下标,它所指向的是元素写入时应该被存放的位置。
  • recvx,uint,同sendx,但是指向的是要被读取元素的位置。
  • recvq,waitq,等待读取的被阻塞的协程队列。
  • sendq,waitq,等待写入的被阻塞的协程队列。
  • lock,mutex,互斥锁。在同一时刻下,channel。只会被一个协程读写。

创建 channel 时,所调用的make(chan int,10)语句中,int 对应的即为 elemtype,10对应的即为dataqsiz 。

2 使用方法

  1. 初始化

初始化一个 channel 有两种方法:var c chan int,该 channel 会被初始化为 nil;c := make(chan int,10),它会创建一个对应的 channel 赋值给c,该 channel 是有缓冲区的。如果传入 make 函数的长度为0,那么该 channel 是无缓冲区的。

  1. 操作符

<- 为channel的操作符。如果 channel 变量在左表示数据进入 channel ,反之 channel 变量在右则表示数据从 channel 中读取。该操作符还可以限制 channel 的读写类型。函数参数中可以有<- channel类型的参数和channel <-类型的参数,分别是在函数内只能读和只能写的 channel。

另外,从上文的数据结构中可以看出来,go 的底层并没有对 channel 的读写做限制,所以这里只是一个类型限制。

  1. 读写数据
  • 如果是无缓冲区的 channel,那么读写只有在存在另一个协程进行对应的操作时才不会被阻塞,反之,都会被阻塞。假设有a,b两个协程和c这个无缓冲区的 channel,a先试图向c中写入数据,那么a会被阻塞,直到b来读取c的数据时被唤醒;反之,b先来读取数据的时候也会被阻塞,直到a来写入数据时被唤醒。
  • 如果是有缓冲区的channel,在channel自身的数据没存满缓冲区时,写不会被阻塞。如果数据已经存满缓冲区,那么写操作和无缓冲区的channel相同。类似的,如果缓冲区内没有数据,那么读取操作也和无缓冲区channel相同。
  • 如果channel未被初始化,那么无论读写都会被永久阻塞
  • 读取数据有两种方式:a := <- ca, ok := <- c。ok变量用于指示数据是否读取成功,但并不指示 channel 是否关闭。如果 channel 为开启状态,那么读取要么有数据要么阻塞。反之,channel 为关闭状态,如果 channel 内还有数据,那么读取成功。只有在 channel 被关闭且 channel 内无数据,ok才会为false。
  1. 关闭channel
  • 可以通过 close 函数来关闭一个 channel,关闭之后的 channel 仍然可读,但是试图写入会引发 panic。
  1. 其他操作
  • len 函数可以获取 channel 内部元素的个数,cap 函数可以获取 channel 缓冲区大小。

3 读写原理

互斥锁加锁解锁就不说了,每次操作都会有。

channel 在写入时如果缓冲区有空间可以写,那么数据会进入缓冲区。反之,如果没有空间,那么协程会进入等待写入队列并且被休眠。如果等待读取队列不为空,那么数据会直接返回给等待读取的协程,而不进入缓冲区。

相同的,对 channel 进行读取时,如果缓冲区有数据可以读,那就从缓冲区取出数据,否则,读取协程进入等待队列睡眠,等待唤醒。如果待写队列不为空,那么数据会直接进入读取协程,不再进入缓冲区。

关闭 channel 时,待读队列内的所有协程会被唤醒,并且获取到 channel 对应类型的默认值;待写队列内的所有协程也会被唤醒,但是会引发 panic。如果关闭一个已经被关闭的 channel,也会引发 panic。

4 其他信息

  1. for-range 语句可以用于从 channel 中读取数据,channel 中没有数据时该协程会被阻塞。
  2. select 可以用于监控多个 channel,当其中一个 channel 可用时触发对应分支,否则进入 default。
  3. 缓冲区长度为1的 channel,可以当互斥锁用。没有缓冲区的 channel,可以当作两个 goroutine 进行同步的小工具。

本文作者:御坂19327号

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!