侧边栏壁纸
博主头像
秋码记录

一个游离于山间之上的Java爱好者 | A Java lover living in the mountains

  • 累计撰写 29 篇文章
  • 累计创建 40 个标签
  • 累计创建 185 个分类

Go 语言中的带有缓冲 Channel(Let's Go 三十一)

无缓冲信道 Channel 是无法保存任何值的,该类型信道要求 发送 goroutine 和 接受 goroutine 两者同时准备好,这样才能完成发送与接受的操作。

1、无缓冲 Channel

前几篇文章我们使用 make 创建出来的信道 Channel,并没有传入第二个参数,也就是说,没有传入第二个参数创建的信道 Channel,便是无缓冲信道咯。

无缓冲信道 Channel 是无法保存任何值的,该类型信道要求 发送 goroutine 和 接受 goroutine 两者同时准备好,这样才能完成发送与接受的操作。

假使 两者 goroutine 未能同时准备好,信道便会先执行 发送 和 接受 的操作, goroutine 会阻塞等待。这种对信道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

阻塞指的是由于某种原因数据没有到达,当前协程(线程)持续处于等待状态,直到条件满足才解除阻塞。

同步指的是在两个或多个协程(线程)之间,保持数据内容一致性的机制。

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// wg 用来等待程序结束
var wg sync.WaitGroup

func init() {
    rand.Seed(time.Now().UnixNano())
}

func main() {
    // 创建一个无缓冲的通道
    court := make(chan int)
    // 计数加 2,表示要等待两个goroutine
    wg.Add(2)
    // 启动两个选手
    go player("Nadal", court)
    go player("Djokovic", court)
    // 发球
    court <- 1
    // 等待游戏结束
    wg.Wait()
}

// player 模拟一个选手在打网球
func player(name string, court chan int) {
    // 在函数退出时调用Done 来通知main 函数工作已经完成
    defer wg.Done()
    for {
        // 等待球被击打过来
        ball, ok := <-court
        if !ok {
            // 如果通道被关闭,我们就赢了
            fmt.Printf("Player %s Won\n", name)
            return
        }
        // 选随机数,然后用这个数来判断我们是否丢球
        n := rand.Intn(100)
        if n%13 == 0 {
            fmt.Printf("Player %s Missed\n", name)
            // 关闭通道,表示我们输了
            close(court)
            return
        }
        // 显示击球数,并将击球数加1
        fmt.Printf("Player %s Hit %d\n", name, ball)
        ball++
        // 将球打向对手
        court <- ball
    }
}

img

2、有缓冲 Channel

有缓冲的 Channel )是一种在被接收前能存储一个或者多个值。这种类型的信道并不强制要求 goroutine 之间必须同时完成发送和接收。信道会阻塞发送和接收动作的条件也会不同。只有在信道中没有要接收的值时,接收动作才会阻塞。只有在信道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

这导致有缓冲的信道和无缓冲的信道之间的一个很大的不同:无缓冲的信道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的信道没有这种保证。

在无缓冲通道的基础上,为信道增加一个有限大小的存储空间形成带缓冲信道。带缓冲信道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到信道中没有数据可读时,信道将会再度阻塞。

varChan := make(chan chanType,chanSize)
//chanSize 决定可以保存多少大小的元素数量
package main

import (
    "fmt"
)

func main() {
    // 创建一个4个元素缓冲大小的 string 通道
    ch := make(chan string, 4)
    // 查看当前通道的大小
    fmt.Println(len(ch))
    // 发送4个 string 元素到通道
    ch <- "秋码记录"
    ch <- "https://qiucode.cn"
    ch <- "你我杂志刊"
    ch <- "仗剑行于江湖"
    // 查看当前通道的大小
    fmt.Println(len(ch))
}

img

带缓冲信道在很多特性上和无缓冲信道是类似的。无缓冲信道可以看作是长度永远为 0 的带缓冲信道。因此根据这个特性,带缓冲信道在下面列举的情况下依然会发生阻塞:

  • 带缓冲信道被填满时,尝试再次发送数据时发生阻塞。
  • 带缓冲信道为空时,尝试接收数据时发生阻塞。

为什么Go语言对信道要限制长度而不提供无限长度的信道?

我们知道信道(channel)是在两个 goroutine 间通信的桥梁。使用 goroutine 的代码必然有一方提供数据,一方消费数据。当提供数据一方的数据供给速度大于消费方的数据处理速度时,如果信道不限制长度,那么内存将不断膨胀直到应用崩溃。因此,限制信道的长度有利于约束数据提供方的供给速度,供给数据量必须在消费方处理量+信道长度的范围内,才能正常地处理数据。