go语法:chan通道

参考:

https://www.jianshu.com/p/24ede9e90490(简书:由浅入深剖析go channel)

https://zhuanlan.zhihu.com/p/32521576(知乎:go channel 最佳实践)

https://blog.csdn.net/qq_35976351/article/details/81984117(Golang关于channel死锁情况的汇总以及解决方案)

channel内部结构

每个channel内部实现都有三个队列

  1. 接收消息的协程队列。这个队列的结构是一个限定最大长度的链表,所有阻塞在channel的接收操作的协程都会被放在这个队列里。
  2. 发送消息的协程队列。这个队列的结构也是一个限定最大长度的链表。所有阻塞在channel的发送操作的协程也都会被放在这个队列里。
  3. 环形数据缓冲队列。这个环形数组的大小就是channel的容量。如果数组装满了,就表示channel满了,如果数组里一个值也没有,就表示channel是空的。对于一个阻塞型channel来说,它总是同时处于即满又空的状态。

一个channel被所有使用它的协程所引用,也就是说,只要这两个装了协程的队列长度大于零,那么这个channel就永远不会被垃圾回收。另外,协程本身如果阻塞在channel的读写操作上,这个协程也永远不会被垃圾回收,即使这个channel只会被这一个协程所引用。

channel的发送和接收流程

向 channel 写数据的流程:

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

向 channel 读数据的流程:

  1.  发送队列是不为空缓冲区从 sendq 中取出 G,把 G 中数据读出,最后把 G 唤醒
  2. 发送队列是不为空且缓冲区已满,缓冲区取出数据从 sendq 中取出 G,把 G 中数据写入缓冲区尾部,最后把 G 唤醒
  3. 缓冲区数据缓冲区读取
  4. 缓冲区缓冲区无数据G加入接收队列进入睡眠等待唤醒
  1. channel的类型

通道3种形式

var ch chan string; // nil channel
ch := make(chan string); // zero channel,阻塞型(读和写同时进行,并且读写不能在同一个协程中)
ch := make(chan string, 10); // buffered channel,非阻塞性(满了阻塞写,空了阻塞读)

 

channel简单规则表

下标的活跃Channel表示即非空又非关闭的Channel,缓冲与非缓存一样

  空channel 已关闭channel 活跃channel
close(ch) 取决于是否已关闭 panic 成功
ch  <-  v 阻塞 panic 成功或阻塞
v,ok <- ch 阻塞 永不阻塞 成功或阻塞

channel规则详细解释

空channel

在空channel上的调用len和cap函数都统一返回零。

已关闭的Channel

  1. 关闭一个已关闭的channel会引发panic
  2. 向一个已关闭的channel发送值会引发panic。当这种send操作处于select块里面的case语句上时,它会随时导致select语句引发panic。
  3. 从一个已关闭的channel上接收值既不会阻塞也不能panic,它一直能成功返回。只是返回的第二个值ok永远是false,表示接收到的v是在channel关闭之后拿到的,对应得值也是相应元素类型的零值。可以无限循环从已关闭的channel上接收值。

活跃的Channel

关闭操作

  1. 从channel的接收协程队列中移除所有的goroutine,并唤醒它们。
  2. 从channel的接收协程队列中移除所有的goroutine,并唤醒它们。
  3. 一个已关闭的channel内部的缓冲数组可能不是空的,没有接收的这些值会导致channel对象永远不会被垃圾回收。

发送操作

  1. 如果是阻塞型channel,那就从channel的接收协程队列中移出第一个协程,然后把发送的值直接递给这个协程。
  2. 如果是阻塞型channel,并且channel的接收协程队列是空的,那么当前的协程将会阻塞,并进入到channel的发送协程队列里。
  3. 如果是缓冲型channel,并且缓冲数组里还有空间,那么将发送的值添加到数组最后,当前协程不阻塞。
  4. 如果是缓冲型channel,并且缓冲数组已经满了,那么当前的协程将会阻塞,并进入到channel的发送协程队列中。

接收操作

  1. 如果是缓冲型channel,并且缓冲数组有值,那么当前的协程不会阻塞,直接从数组中拿出第一个值。如果发送队列非空,还需要将队列中的第一个goroutine唤醒。
  2. 如果是阻塞型channel,并且发送队列非空的话,那么唤醒发送队列第一个协程,该协程会将发送的值直接递给接收的协程。
  3. 如果是缓冲型channel,并且缓冲数组为空,或者是阻塞型channel,并且发送协程队列为空,那么当前协程将会阻塞,并加入到channel的接收协程队列中。

死锁和解决办法

死锁场景

1,主协程读取无数据的channel

 

2,主协程向已写满的channel中写入

解决方法

1,使用select

2,在子协程操作channel

 

判断channel是否关闭

show me the the code

c := make(chan int, 10)
    c <- 1
    c <- 2
    c <- 3
    close(c) // 这里就关闭了channel,下面依然可以读取到channel里面残留的数据,直到数据处理完成后才读取到关闭消息
    // c <- 4    // 本行代码不可执行,向一个已关闭的通道压入数据时会造成panic// 总结
    // 1. 第二个字段的值为true时,channel可能没关闭,也可能已经关闭,不能证明什么
    // 2. 第二个字段为false时,可以证明channel中已没有残留数据且已关闭
 
    for {
        i, isOpen := <-c
        if !isOpen { // 若不加本判断,从一个已关闭的channel中读取数据会直接返回,且是默认值,造成死循环!!!!!!
            fmt.Println("channel 已关闭!")
            break
        }
        fmt.Printf("i=%v, isOpen=%v \n", i, isOpen)
    }

 

 

 

 

 

posted @ 2021-08-31 17:34  指令跳动  阅读(201)  评论(0编辑  收藏  举报