golang-channel

什么是 channel 管道

channel 是 goroutine 与 goroutine 之间通信的重要桥梁

channel 是一个通道,用于端到端的数据传输,这有点像我们平常使用的消息队列,只不过 channel 的发送方和接受方是 goroutine 对象,属于内存级别的通信。

这里涉及到了 goroutine 概念,goroutine 是轻量级的协程,有属于自己的栈空间。 我们可以把它理解为线程,只不过 goroutine 的性能开销很小,并且在用户态上实现了属于自己的调度模型。

传统的线程通信有很多方式,像内存共享、信号量等。其中内存共享实现较为简单,只需要对变量进行并发控制,加锁即可。但这种在后续业务逐渐复杂时,将很难维护,耦合性也比较强。

后来提出了 CSP 模型,即在通信双方抽象出中间层,数据的流转由中间层来控制,通信双方只负责数据的发送和接收,从而实现了数据的共享,这就是所谓的通过通信来共享内存。 channel 就是按这个模型来实现的。

channel 在多并发操作里是属于协程安全的,并且遵循了 FIFO 特性。即先执行读取的 goroutine 会先获取到数据,先发送数据的 goroutine 会先输入数据。

另外,channel 的使用将会引起 Go runtime 的调度调用,会有阻塞和唤起 goroutine 的情况产生。

channel特性特性

1、channel,可译为通道,是go语言协程goroutine之间的通信方式。
2、channel通信可以想象成从管道的一头塞进数据,从另一头读取数据。
3、协程通过channel通信可以不用进行加锁操作。
4、把数据发往无缓冲通道,如果接收方没有接收。发送操作将持续阻塞,此时会 释放cpu,执行其他协程,并且查看其他携程是否能够解除阻塞
5、接收将持续阻塞直到发送方发送数据

channel的2种类型

c1 := make(chan int) // 无缓冲
c2 := make(chan interface{}) // 任意类型通道
c3 := make(chan int, 1) // 有缓冲

type Str struct{}
c4 := make(chan *Str) // 指针类型通道
c5 := make(chan struct{})

无缓冲

ch := make(chan int)

上面是创建了无缓冲的 channel,一旦有 goroutine 往 channel 发送数据,那么当前的 goroutine 会被阻塞住,直到有其他的 goroutine 消费了 channel 里的数据,才能继续运行。

有缓冲

ch := make(chan int, 2)

第二个参数表示 channel 可缓冲数据的容量。只要当前 channel 里的元素总数不大于这个可缓冲容量,则当前的 goroutine 就不会被阻塞住。

阻塞条件:

  1. 通道被填满时,尝试再次发送数据发生阻塞
  2. 通道中没数据时,尝试接收数据时会发生阻塞

nil的channel

我们也可以声明一个 nil 的 channel,只是创建这样的 channel 没有意义,读、写 channel 都将会被阻塞住。一般 nil channel 用在 select 上,让 select 不再从这个 channel 里读取数据,

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        for i := 0; i < 3; i++ {
            time.Sleep(1 * time.Second)
            ch1 <- 2
        }
        fmt.Println("ch1  set null")
        //模拟某些业务场景写入nil
        ch1 = nil
    }()

    go func() {
        for {
            time.Sleep(1 * time.Second)
            ch2 <- 2
        }
    }()

    for {
        select {
        case <-ch1: // 当 ch1 被设置为 nil 后,将不会到达此分支了。
            fmt.Println("ch1 process")
        case <-ch2:
            fmt.Println("ch2 process")
        }
    }
}

输出

ch1 process
ch2 process
ch1 process
ch2 process
ch1  set null
ch1 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
Exiting.
View Code

channel三种模式

只读

只写

channel三种状态

使用例子

创建

带缓冲
ch := make(chan int,3)
不带缓冲
ch := make(chan int)

创建时会做一些检查:

元素大小不能超过64K
元素对齐大小不能超过maxAlign(8字节)
计算出来的内存是否超过限制
创建时的策略:

无缓冲的channel——会直接给hchan分配内存
有缓冲的channel并且元素不包含指针(buf指针,指向底层数组)——会为hchan和底层数组分配一段连续的地址
有缓冲的channel并且元素包含指针——会为hchan和底层数组分别分配地址

发送

包括检查和数据发送两个步骤

数据发送步骤

1.如果channel的读等待队列存在接收者goroutine(有发送者goroutine阻塞)

将数据直接发送给第一个等待的goroutine,唤醒接收的goroutine

2.如果channel的读等待队列不存在接收者goroutine(无有发送者goroutine阻塞)

如果buf指向的循环数组未满,会把数据发送到循环数组的队尾

如果buf指向的循环数组已满,就会阻塞,将当前goroutine加入写等待队列,并挂起等待唤醒

func chansend(c *hchan,ep unsafe.Pointer,block bool,callerpc uintptr)bool

阻塞式

ch <- 10

非阻塞式

select {
    case ch <- 10:
    ...
  default

select中的default 会导致select无阻塞,也会导致cpu飙高问题

去掉default,select会阻塞,直到通道有数据时解除

当两个通道同时有数据产生时,选择其中一个通道去执行,直到所有通道数据都处理完毕

接收

包括检查和数据接收两个步骤

数据接收步骤

1.如果channel的写等待队列存在发送者goroutine(有发送者goroutine阻塞)

如果是无缓冲channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的goroutine

如果是有缓冲channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据拷贝到buf指向的循环数组队尾,唤醒发送的goroutine

2.如果channel的写等待队列不存在发送者goroutine(没有发送者goroutine阻塞)

如果buf指向的循环数组非空,将循环数组的队首元素拷贝给接收变量

如果buf指向的循环数组为空,这个时候就会阻塞,将当前goroutine加入读等待队列,并挂起等待唤醒
阻塞式

<-ch
 
v:= <-ch
 
v,ok := <-ch
 
//当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值
for i := range ch {
    fmt.Println(i)
}

非阻塞式

select {
    case  <- ch:
    ...
  default
}

select中的default 会导致select无阻塞,也会导致cpu飙高问题

去掉default,select会阻塞,直到通道有数据时解除

当两个通道同时有数据产生时,选择其中一个通道去执行,直到所有通道数据都处理完毕

channel中的死锁

例子一

无缓冲的channel的读写者必须同时完成发送和接收,而不能串行,显然单协程无法满足。所以这里造成了循环等待,会死锁。

func main(){
    ch := make(chan int)
    ch <- 1
    <- ch
}

 

例子二

往 channel 里读写数据时是有可能被阻塞住的,一旦被阻塞,则需要其他的 goroutine 执行对应的读写操作,才能解除阻塞状态。

然而,阻塞后一直没能发生调度行为,没有可用的 goroutine 可执行,则会一直卡在这个地方,程序就失去执行意义了。此时 Go 就会报 deadlock 错误,如下代码:
读channel和写channel都需要出现,单独出现会死锁

func main(){
    ch := make(chan int)
    ch <- 1
}
func main(){
    ch := make(chan int)
    <-ch
}

channel底层原理

channel 创建后返回了 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 //同步锁-互斥锁
    ...
}
recvq和sendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表

无缓冲channel先写再读

1.G1往channel写数据,由于 channel 是无缓冲的,所以 G1 暂时被挂在 sendq 队列里,然后 G1 调用了 gopark 休眠了起来。

2.g2从队列里读数据

G2 发现 sendq 等待队列里有 goroutine 存在,于是直接从 G1 copy 数据过来,并且会对 G1 设置 goready 函数,这样下次调度发生时, G1 就可以继续运行,并且会从等待队列里移除掉。

注意:缓存链表中以上每一步的操作,都是需要加锁操作的!

  • 每一加粗样式步的操作的细节可以细化为:
  • 第一,加锁
  • 第二,把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)
  • 第三,释放锁

无缓冲channel 先读再写

1.G1读取时发现sendq没有goroutline存在,G1 暂时被挂在了 recvq 队列,然后休眠起来。

 2.G2 在写数据时,发现 recvq 队列有 goroutine 存在,于是直接将数据发送给 G1。同时设置 G1 goready 函数,等待下次调度运行。

有缓冲channel先写再读

1.优先判断缓冲数据区域是否已满,如果未满,则将数据保存在缓冲数据区域,即环形队列里。如果已满,则和之前的流程是一样的。

 

 2.当 G2 要读取数据时,会优先从缓冲数据区域去读取,并且在读取完后,会检查 sendq 队列,如果 goroutine 有等待队列,则会将它上面的 data 补充到缓冲数据区域,并且也对其设置 goready 函数。

有缓存先读后写

流程一样

使用注意事项

写数据注意

向一个nil channel发送数据,会调用gopark函数将当前goroutine挂起
向一个已经关闭的channel发送数据,直接会panic
如果channel的recvq当前队列中有被阻塞的接收者,则直接将数据发送给当前goroutine
当channel的缓冲区还有空闲空间,则将数据发送到sendx指向缓冲区的位置
当没有缓冲区或者缓冲区满了,则会创建一个sudog的结构体将其放到channel的sendq队列当中陷入休眠等待被唤醒

读数据注意

从一个nil channel接收数据,会调用gopark函数将当前goroutine挂起,让出处理器的使用权
从一个已经关闭并且缓冲区中没有元素的channel中接收数据,则会接收到该类型的默认元素,并且第二个返回值返回false
如果channel没有缓冲区且sendq的队列有阻塞的goroutine,则把sendq队列头的sudog中保存的元素值copy到目标地址中
如果channel有缓冲区且缓冲区里面有元素,则把recvx指向缓冲区的元素值copy到目标地址当中,sendq队列头的sudog的元素值copy到recvx指向缓冲区位置的地址当中
当上面的条件都不符合时,则会创建一个sudog的结构体将其放到channel的recvq队列当中陷入休眠等待被唤醒

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章