Golang-channel实现

一.简介

       如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信系统,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特定的类型,也就是channels可发送数据的类型。

二.使用

2.1 创建channel

ch := make(chan int)

2.2 channel读写

ch := make(chan int)

// write to channel
ch <- x

// read from channel
x <- ch

// another way to read
x = <- ch

channel 一定要初始化后才能进行读写操作,否则会永久阻塞。

2.3 关闭 channel

ch := make(chan int)

close(ch)

有关 channel 的关闭,你需要注意以下事项:

  • 关闭一个未初始化(nil) 的 channel 会产生 panic
  • 重复关闭同一个 channel 会产生 panic
  • 向一个已关闭的 channel 中发送消息会产生 panic
  • 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
  • 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息

三.channel的两种类型

3.1 有缓冲channel

ch := make(chan int, 10)

       有缓存的 channel 类似一个阻塞队列(采用环形数组实现)。当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。

3.2 无缓冲channel

ch := make(chan int)

       从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;同理,向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。

3.3  select 使用

select 用法类似 IO 多路复用,可以同时监听多个 channel 的消息状态,看下面的例子

select {
    case <- ch1:
    ...
    case <- ch2:
    ...
    case ch3 <- 10;
    ...
    default:
    ...
}

 ​​​​​​

msgCh := make(chan struct{})
quitCh := make(chan struct{})
for {
    select {
    case <- msgCh:
        doWork()
    case <- quitCh:
        finish()
        return
}
  • select 可以同时监听多个 channel 的写入或读取
  • 执行 select 时,若只有一个 case 通过(不阻塞),则执行这个 case 块
  • 若有多个 case 通过,则随机挑选一个 case 执行
  • 若所有 case 均阻塞,且定义了 default 模块,则执行 default 模块。若未定义 default 模块,则 select 语句阻塞,直到有 case 被唤醒。
  • 使用 break 会跳出 select 块。

三.底层实现

2.1 hchan结构

type hchan struct {
   qcount   uint           // 队列中当前数据的个数
   dataqsiz uint           // size of the circular queue
   buf      unsafe.Pointer // 数据缓冲区,存放数据的环形数组
   elemsize uint16 // channel中数据类型的大小(单个元素的大小)
   closed   uint32 // 表示channel是否关闭标识位
   elemtype *_type // 队列中的元素类型
   sendx    uint   // 当前发送元素的索引
   recvx    uint   // 当前接收元素的索引
   recvq    waitq  // 接受等待队列,由recv行为(也就是<-ch)阻塞在channel上的goroutine队列
   sendq    waitq  // 发送等待队列, 由send行为(也就是ch<-)阻塞在channel上的goroutine队列
   //lock保护chann中的所有字段,以及在此通道上阻塞的sudoG中的几个字段。
   //保持此锁时不要更改另一个G状态(特别是没准备好G),因为这可能会因堆栈收缩而死锁
   lock mutex
}

//发送及接收队列的·1结构体
type waitq struct {
    first *sudog
    last  *sudog
}
  • qcount uint // 当前队列中剩余元素个数。
  • dataqsiz uint // 环形队列长度,即缓冲区的大小,即make(chan T,N),N。
  • buf unsafe.Pointer // 环形队列指针。
  • elemsize uint16 // 每个元素的大小。
  • closed uint32 // 表示当前通道是否处于关闭状态。创建通道后,该字段设置为0,即通道打开; 通过调用close将其设置为1,通道关闭。
  • elemtype *_type // 元素类型,用于数据传递过程中的赋值。
  • sendx uint 和 recvx uint是环形缓冲区的状态字段,它指示缓冲区的当前索引 - 支持数组,它可以从中发送数据和接收数据。
  • recvq waitq // 等待读消息的goroutine队列。
  • sendq waitq // 等待写消息的goroutine队列。
  • lock mutex // 互斥锁,为每个读写操作锁定通道,因为发送和接收必须是互斥操作。

2.2 创建过程

2.2.1 写入操作

1.创建带buffer的channel

2.向channel中写入数据

3.3 写入过程如下:

  • 锁定整个管道结构。
  • 确定写入,尝试从等会带队列等待goroutine,然后将元素直接写入goroutine。
  • 如果recvq为空,则确定缓冲区是否可用。如果可用,从当前goroutine复制数据到缓冲区。
  • 如果缓冲区已满,则要写入的元素将保存在当前正在执行的goroutine结构中,并且当前goroutine将在sendq中排队并从运行中挂起。
  • 写入完成释放锁。

2.2.2 读取过程

  • 先读取channel全局锁。
  • 尝试sendq从等待队列中获取等待的goroutine。
  • 如果有等待的goroutine,且有缓冲区(缓冲区已满),从缓冲区队首取出数据,再从sendq取出一个goroutine。将goroutine中数据存入buf对位,结束读取释放锁。
  • 如没有后等待的goroutine,且缓冲区有数据,直接读取缓冲区数据,解释读取释放锁。
  • 如果没有等待的goroutine,且没有缓冲或缓冲区域为空,将当前的goroutine加入denq排队,进入睡眠,等待被写goroutine唤醒。结束释放锁。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值