go原理刨析之channel

发布于:2025-04-06 ⋅ 阅读:(7) ⋅ 点赞:(0)

提起go里边的管道无人不知无人不晓,go原理中推荐的就是通过管道来通信,而不是通过共享内存来通信。
我们先创建一个channel
channel的初始化有两种:

// 这种方式只声明不初始化
var ch chan int 
// 使用make 
ch1 := make(chan string) // 无缓冲的channel
ch2 := make(chan string,5) // 有缓冲的channel

此外还有channel 特有的操作符

ch := make(chan int,10)
ch <- 1  // 数据流入管道
for _,v := range ch {
	fmt.Println(v)// 数据流出管道
}

一般使用内置函数close 关闭 channel
内置函数len cap 分别查询缓冲区中数据的个数以及缓冲区的大小

当管道是无缓冲区的管道的时候,从管道中读取和写入都会阻塞,知道有写成从管道写入或读取数据

管道有缓冲区但缓冲区无数据的时候读取也会阻塞,直到有协程写入数据。类似向管道写入数据如果缓冲区已满的话也是无法写入的

对于值为nil的管道,无论读写都会阻塞,永久阻塞
关闭已经关闭的管道或者是向已经关闭的管道中进行写入会触发Panic

管道读取的时候类似map 最多可以有两个返回值,第一个表示读出的数据,第二个表示是否成功读取了数据

管道事一种先入先出的队列 FIFO 数据总是按照写入的顺序流出管道

协程读取管道时,阻塞的条件有:

  • 管道无缓冲区
  • 管道缓冲区无数据
  • 管道初始化为nil,但并未分配内存

协程写入管道,阻塞的条件有:

  • 管道无缓冲区
  • 管道缓冲区已满
  • 管道初始化为nil,但并未分配内存
管道内部结构以及底层原理

channel 的源码在src/runtime/chan.go:chan
在这里插入图片描述

type hchan struct {
	qcount   uint           // 当前队列中剩余的元素
	dataqsiz uint           // 环形队列的长度,即可以存放的元素个数
	buf      unsafe.Pointer // points to an array of dataqsiz elements 指向内存的指针
	elemsize uint16  // 每个元素的大小
	synctest bool // true if created in a synctest bubble 
	closed   uint32  // 标识关闭
	timer    *timer // timer feeding this chan
	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  // 互斥锁 chan不允许并发读写
}

buf 指针指向的是一个环形队列做为缓冲区
使用数组来实现这个队列
sendx,recvx分别表示队尾和对首
dataqsiz指示队列长度为6,即可以缓冲6个元素
qcount 表示目前队列中还有两个元素

等待队列

从管道读取数据时,如果管道缓冲区为空或者没有缓冲区,当前协程会被阻塞,并加入recvq队列
向管道写入数据,如果缓冲区已满或者没有缓冲区,则当前协程会被阻塞并加入sendq队列

处于等待队列的协程会在其他协程操作管道时被唤醒:

  • 因读阻塞的协程会被向管道写入数据的协程唤醒
  • 因写阻塞的协程会被从管道读取数据的协程唤醒
    一般情况下recvq snedq 一般至少一个为空,只有一个例外,那就是同一个协程使用select语句向管道一边写入数据。一边读取数据,此时协程会分别位于两个等待队列中
创建 写数据 读取数据 时内部流程

创建管道:
就是初始化hchan结构体
缓存区长度由内置函数make设置
buf的大小由元素大小和缓冲区长度共同决定

向管道中写入数据:

  • 如果缓冲区有空余位置,则将数据写入缓冲区,结束发送过程
  • 如果缓冲区没有空余位置,将协程加入sendq队列,等到读协程唤醒

-当recvq不为空时说明有一个协程在等待接收数据 很明显此时缓冲区为空,那么写协程进入后不用写入缓冲区,直接传递给recvq中的第一个

从管道读数据:

  • 如果缓冲区有数据,直接读出
  • 如果缓冲区没有数九,将协程加入recvq队列,进入睡眠并等待被写协程唤醒
    同上在sendq不为空的情况下,且没有缓冲区,直接获取数据,不用写入缓冲区

关闭管道时:
关闭管道时会把recvq中的协程全部唤醒,这些协程获取的数据都为对应类型的灵芝,sendq的协程也会唤醒,但是相当于向关闭的channel ,里边写数据有可能触发Panic

会触发panic的操作:
关闭值为nil的管道
关闭已经被关闭的管道
向已经关闭的管道写入数据


网站公告

今日签到

点亮在社区的每一天
去签到