Go中的Channel的底层是怎么实现的?
- Go中的Channel的底层是怎么实现的?
- CSP模型是什么?
- CSP是一种并发模型,他以通信的方式来共享内存,而不同于传统的多线程通过共享内存进行通信的方式。
 
 - channel的底层数据结构是什么?
- type hchan struct { qcount uint // 队列中的总元素个数 dataqsiz uint // 环形队列大小,即可存放元素的个数 buf unsafe.Pointer // 环形队列指针(只针对有缓冲区的channel) elemsize uint16 //channel中每个元素的大小 closed uint32 //标识channel的关闭状态 elemtype *_type // channel中数据的元素类型 sendx uint // 发送索引,元素写入时存放到队列中的位置 recvx uint // 接收索引,元素从队列的该位置读出 recvq waitq // 等待读消息的goroutine队列 sendq waitq // 等待写消息的goroutine队列 lock mutex //互斥锁,chan不允许并发读写 }
 
 - channel为什么可以做到线程安全?
- Channel 可以理解是一个先进先出的队列,channel底层也维护了一个锁,在出队入队的时候都加锁了。所以说通过管道进行通信,发送一个数据到Channel和从Channel接收一个数据都是原子性的。
 
 - 向channel发送数据的过程是怎么样的?
- 1.如果channel的读等待队列存在接收者 (1)说明有协程在等待读取数据,在排队,是读多写少。就说明缓冲区中无数据或无缓冲区,将直接从 recvq取出一个G(协程) ,并把数据写入协程中,最后把该 G 唤醒,结束发送过程。 2. 如果channel的读等待队列不存在接收者 (1)若循环数组中有空余位置,则将数据写入缓冲区,结束发送过程。 (2)若循环数组中没有空余位置,则将当前 G 加入 sendq (等待读请求的队列),协程被挂起,等待被读 goroutine 唤醒。
 
 - 从channel读取数据的过程是怎么样的?
- 1.如果 channel 的写等待队列存在发送者goroutine。 (1)如果是无缓冲 channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的 goroutine。 (2)如果是有缓冲 channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者gorutine的数据拷贝到buf循环数组队尾,唤醒发送的 goroutine。 2.如果 channel的写等待队列不存在发送者goroutine (1)如果循环数组buf非空,将循环数组buf的队首元素持贝给接收变量 (2)如果循环数组buf为空,这个时候就会走阻塞接收的流程,将当前 goroutine 加入读等待队列,并挂起等待唤醒
 
 - 关闭一个channel的过程是怎么样的?
- 1.加锁,然后修改关闭状态, 2.关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic(所以,在不了解 channel 还有没有接收者的情况下,不能贸然关闭 channel)。
- 关闭channel会发生panic 出现的场景还有: 1.关闭值为 nil 的 channel 2.关闭已经关闭的 channel 3.向已经关闭的 channel 中写数据
 
 
 - 1.加锁,然后修改关闭状态, 2.关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic(所以,在不了解 channel 还有没有接收者的情况下,不能贸然关闭 channel)。
 - 无缓冲的channel的发送和接受是同步的吗?
- channel 无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据; channel 有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
 
 - 无缓冲的channel和有缓冲的channel的区别
- 对于无缓冲的channel,发送方会阻塞该信道,直到接收方从该信道接收到数据为止。如果信道中没有数据,接收方也会进行阻塞,直到发送方将数据发送到信道中。 对于有缓冲的channel,发送方在缓冲区使用完的时候阻塞,接收方在信道为空的时候进行阻塞
 - func main() { st := time.Now() // 记录当前时间 ch := make(chan bool) // 创建一个无缓冲的通道 go func() { time.Sleep(time.Second * 2) // 休眠2秒 <-ch // 从通道接收数据,此处会阻塞,直到有数据可接收 }() ch <- true // 向通道发送数据,发送方会阻塞,直到有接收方接收到数据 fmt.Printf(“花费 %.1f s\n”, time.Now().Sub(st).Seconds()) // 花费2s }
 - func main() { st := time.Now() ch := make(chan bool, 2) go func () { time.Sleep(time.Second * 2) <-ch }() ch <- true ch <- true // 缓冲区为 2,发送方不阻塞,继续往下执行 fmt.Printf(“cost %.1f s\n”, time.Now().Sub(st).Seconds()) // cost 0.0 s ch <- true // 缓冲区使用完,发送方阻塞,2s 后接收方接收到数据,释放一个插槽,继续往下执行 fmt.Printf(“cost %.1f s\n”, time.Now().Sub(st).Seconds()) // cost 2.0 s }
 
 - channel如何控制协程的并发执行顺序?
- 我们知道,多个协程并发执行的时候,执行的时候不能保证协程的先后执行顺序,但是我们可以用channel来控制协程的执行顺序。 比如说现在有三个协程要进行顺序控制,我们就可以创建三个channel,然后等协程1执行完,再给协程2发消息,因为是阻塞等待吗,依次类推就可以了,并且有一个优化的点是可以用空结构体,消耗资源少。
 
 - 从一个关闭的channel中可以读出数据吗?
- //从一个有缓冲的 channel 里读数据,当 channel 被关闭,依然能读出有效值。 //只有当返回的 ok 为 false 时,读出的数据才是无效的。 func main() { ch := make(chan int, 5) ch <- 18 close(ch) x, ok := <-ch if ok { fmt.Println(“received: “, x)//received: 18 } x, ok = <-ch//这时候ch里已经没有数据了,所以返回的就是false if !ok { fmt.Println(“channel closed, data invalid.”)//channel closed, data invalid. } }
 
 - 如何优雅的关闭channel?
- 根据 sender 和 receiver 的个数,分下面几种情况: 1.一个 sender,一个 receiver 2.一个 sender, M 个 receiver 3.N 个 sender,一个 reciver 4.N 个 sender, M 个 receiver 对于 1,2,只有一个 sender 的情况就不用说了,直接从 sender 端关闭就好了,没有问题。重点关注第 3,4 种情况。
- 第三种情况下的方式是: 增加一个传递关闭信号的 channel,receiver 通过信号 channel 下达关闭数据 channel 指令。senders 监听到关闭信号后,停止发送数据。(由于channel是线程安全的,所以可以这样做)
 - 第四种情况下的方式是: 和第 3 种情况不同,这里有 M 个 receiver,如果直接还是采取第 3 种解决方案,可能会出现多次关闭的情况,因为receiver之间对关闭channel是无感的,因此需要增加一个中间人,M 个 receiver 都向它发送关闭 dataCh 的“请求”,中间人收到第一个请求后,就会直接下达关闭 dataCh 的指令(通过关闭 stopCh,这时就不会发生重复关闭的情况,因为 stopCh 的发送方只有中间人一个)。另外,这里的 N 个 sender 也可以向中间人发送关闭 dataCh 的请求。
 
 
 - 根据 sender 和 receiver 的个数,分下面几种情况: 1.一个 sender,一个 receiver 2.一个 sender, M 个 receiver 3.N 个 sender,一个 reciver 4.N 个 sender, M 个 receiver 对于 1,2,只有一个 sender 的情况就不用说了,直接从 sender 端关闭就好了,没有问题。重点关注第 3,4 种情况。
 - channel在什么情况下会出现资源泄露的状况?
- channel由于有协程的阻塞队列,可能会导致协程泄漏,比如说协程在发送或者接受的时候进行阻塞,但是channel一致是满的或者空的,协程就会一直阻塞,这时候协程就发生泄漏了。
 
 - channel有哪些应用呢?
- 停止信号
- 作为停止信号使用,比如优雅的关闭channel,在上面,可以引导着说
 
 - 任务定时
- 超时控制
- func main() { var stopChannel = make(chan struct{}, 1) var fun = func() { select { /* //定时2s,阻塞2s,2s后产生一个事件,往time这个channel写内容(写了也没啥用) <-time.After(2 * time.Second) 还有的就是go的switch语句自带break */ case <-time.After(100 * time.Millisecond): println(“1如果超过100毫秒就可以打印这个了”) case <-stopChannel: println(“2如果等待在100毫秒之前就打印这个”) } } go fun() stopChannel <- struct{}{} time.Sleep(50 * time.Millisecond) //给fun函数一点执行时间。 } 这时候会打印(2如果等待在100毫秒之前就打印这个) 如果加点睡眠时间就会打印1了
 
 - 定期执行某个任务
- func worker() { ticker := time.Tick(1 * time.Second) for { select { case <- ticker: // 执行定时任务 fmt.Println(“执行 1s 定时任务”) } } } 每隔 1 秒种,执行一次定时任务。
 
 
 - 超时控制
 - 解耦生产者和消费者
- 这个就是做消息队列了
 
 - 控制并发数
- 在某些场景下,需要执行大量任务,但又不能一次性将所有任务同时发送给第三方资源或服务。可能是因为第三方资源有并发访问限制,或者为了避免资源过载,需要控制请求的速率
-  var limit = make(chan int, 3) // 创建一个带有缓冲大小为3的整数通道作为并发限制 for _, w := range work { go func() { limit <- 1 // 在执行任务之前,向限制通道发送一个值,占用一个并发槽位 w() // 执行具体的任务 <-limit // 任务完成后,从限制通道接收一个值,释放一个并发槽位 }() }
 
 
 - 在某些场景下,需要执行大量任务,但又不能一次性将所有任务同时发送给第三方资源或服务。可能是因为第三方资源有并发访问限制,或者为了避免资源过载,需要控制请求的速率
 
 - 停止信号
 
 - CSP模型是什么?
 
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 是小白菜哦!

