Channel的底层实现

218 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情

并发是指逻辑上具备同时处理多个任务的能力;并行则是物理上同时执行多个任务。

大多数的编程语言的并发编程模型是基于线程和内存同步访问控制,Go 的并发编程的模型则用 goroutine 和 channel 来替代。Goroutine 和线程类似,channel 和 mutex (用于内存同步访问控制)类似。

1. Channel设计的目标

channel主要用来设计进行多任务间的数据传递,是线程安全的。Channel中发送数据到Channel和从Channel接受一个数据都是原子性

使用场景:goroutine之间的消息传递,并发控制,事件订阅和广播

2. Channel的底层实现

channel是golang中用来实现多个goroutine通信的管道,它的底层是一个叫做hchan的结构体。在go的runtime包下。

channel 的底层就是通过 mutex 来控制并发的。只是 channel 是更高一层次的并发编程原语,封装了更多的功能。

2.1数据结构

type hchan struct {
   qcount   uint            // 循环数组中的元素数量
   dataqsiz uint            // 循环数组的长度
   buf      unsafe.Pointer  // 指向底层循环数组的指针
   elemsize uint16  // chan 中元素大小
   closed   uint32  // chan 是否被关闭的标志
   elemtype *_type  // channel中的元素类型
   sendx    uint    // 下一次发送数据的下标位置
   recvx    uint    // 下一次读取数据的下标位置
   recvq    waitq   // 读等待队列
   sendq    waitq   // 写等待队列
​
   lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}

Channel的结构体主要组成部分:

  1. buf:用来保存goroutine之间传递数据的循环链表,只有缓冲型的channel才有

  2. sendx和recvx:用来记录此循环链表当前发送或者接收数据的下标值

  3. sendq和recvq:保存被阻塞的goroutine

    • 当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列
    • 当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列
  4. lock:保证channel写入和读取数据时线程安全的锁

无缓冲和有缓冲区别: 管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。

3. channel的相关操作

口诀:空读写阻塞,写关闭异常,读关闭空零

操作nil channelclosed channelnot nil, not closed channel
closepanicpanic正常关闭
读 <- ch阻塞读到对应类型的零值阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞
写 ch <-阻塞panic阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞

发生 panic 的情况有三种:向一个关闭的 channel 进行写操作;关闭一个 nil 的 channel;重复关闭一个 channel。读、写一个 nil channel 都会被阻塞。

3.1 写数据

向 channel 写数据的流程: 如果等待接收队列 recvq 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq 取出 G,并把数据写入,最后把该 G 唤醒,结束发送过程; 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程; 如果缓冲区中没有空余位置,将待发送数据写入 G,将当前 G 加入 sendq,进入睡眠,等待被读 goroutine 唤醒;

3.2 读数据

向 channel 读数据的流程: 如果等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G,把 G 中数据读出,最后把 G 唤醒,结束读取过程; 如果等待发送队列 sendq 不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程; 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前 goroutine 加入 recvq,进入睡眠,等待被写 goroutine 唤醒;

参考文献

Go面试题(五):图解 Golang Channel 的底层原理

深度解密Go语言之channel

Go专家编程