迹忆客 专注技术分享

当前位置:主页 > 学无止境 > 编程语言 >

Go Channel 缓冲详细介绍

作者:迹忆客 最近更新:2021/11/22 浏览次数:

什么是缓冲通道

我们在Go 语言Channel 通道详解中讨论的所有通道基本上都是无缓冲的。 正如我们在文章中详细讨论的那样,向无缓冲通道的写入和读取都是阻塞的。

可以使用缓冲区创建通道。 仅当缓冲区已满时才会阻止写入到缓冲通道。 类似地,只有当缓冲区为空时,才会阻塞从缓冲区通道进行读取。

可以通过将额外的容量参数传递给指定缓冲区大小的 make 函数来创建缓冲通道。

ch := make(chan type, capacity)  

capacity 参数应大于 0 才能使通道具有缓冲区。 默认情况下,无缓冲通道的 capacity 为 0,因此我们在Go 语言Channel 通道详解中创建通道时省略了容量参数。

下面我们看一个示例

package main

import (  
    "fmt"
)

func main() {  
    ch := make(chan string, 2)
    ch <- "jiyik"
    ch <- "onmpw"
    fmt.Println(<- ch)
    fmt.Println(<- ch)
}

运行示例

在上面的程序中,我们创建了一个容量为2的缓冲通道。由于通道的容量为2,因此可以在不阻塞的情况下将2个字符串写入通道。 我们将 2 个字符串写入通道。并且通道不阻塞。 然后我们读取了 2 个字符串。所以上述程序执行结果如下

Go 缓冲通道示例运行结果

让我们再看一个缓冲通道的示例,其中通道的值从并发的Goroutine中写入并从主 Goroutine 读取。 这个例子将使我们更好地理解何时写入缓冲通道块。

package main

import (  
    "fmt"
    "time"
)

func write(ch chan int) {  
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Println("成功写入 ", i, "到通道中")
    }
    close(ch)
}
func main() {  
    ch := make(chan int, 2)
    go write(ch)
    time.Sleep(2 * time.Second)
    for v := range ch {
        fmt.Println("从通道中读取值 ", v)
        time.Sleep(2 * time.Second)

    }
}

运行示例

首先我们来看上述程序运行结果(也可以点击运行示例查看运行结果)

Go 并发写入通道

下面我们来分析上述结果产生的过程。

在上面的程序中,创建了一个容量为 2 的缓冲通道 ch。 主 Goroutine 将其传递给 write Goroutine。 然后主 Goroutine 休眠 2 秒。 在此期间,write Goroutine 并发运行。 write Goroutine 有一个 for 循环,它将把 0 到 4 的数字写入 ch 通道。 这个缓冲通道的容量是 2,因此写入 Goroutine 将能够立即将值 0 和 1 写入 ch 通道,然后它会阻塞,直到从 ch 通道读取至少一个值。 所以这个程序将立即打印以下 2 行。

成功写入  0 到通道中
成功写入  1 到通道中

打印完以上两行后,write Goroutine 中对 ch 通道的写入被阻塞,直到有人从 ch 通道读取数据。 由于主 Goroutine 在开始从通道读取之前会休眠 2 秒,因此程序在接下来的 2 秒内不会打印任何内容。 主 Goroutine 在 2 秒后唤醒并开始使用第 1 行的 for range 循环从 ch 通道读取。 并打印读取值,然后再次休眠 2 秒,这个循环一直持续到 ch 关闭。 所以程序会在 2 秒后打印以下几行,

从通道中读取值  0
成功写入  2 到通道中

循环这个过程,一直持续到所有值都写入通道并且在write Goroutine 中关闭。


死锁

package main

import (  
    "fmt"
)

func main() {  
    ch := make(chan string, 2)
    ch <- "naveen"
    ch <- "paul"
    ch <- "steve"
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

在上面的程序中,我们将 3 个字符串写入容量为 2 的缓冲通道。 由于通道已超出其容量,写入被阻塞。 现在一些 Goroutine 必须从通道读取才能继续写入,但在这种情况下,没有从该通道读取的并发例程。 因此会出现死锁,程序会在运行时出现以下消息,并发生 panic

Go 死锁


关闭缓冲通道

我们已经在Go 语言Channel 通道详解中讨论了如何关闭通道。 除了我们在那篇文章中学到的知识之外,在关闭缓冲通道时还有一个需要考虑的微妙之处。

可以从已经关闭的缓冲通道中读取数据。 通道将返回已经写入通道的数据,一旦所有数据都被读取,它将返回通道的零值。

下面我们通过一个示例来理解这一点

package main

import (  
    "fmt"
)

func main() {  
    ch := make(chan int, 5)
    ch <- 5
    ch <- 6
    close(ch)
    n, open := <-ch 
    fmt.Printf("Received: %d, open: %t\n", n, open)
    n, open = <-ch 
    fmt.Printf("Received: %d, open: %t\n", n, open)
    n, open = <-ch 
    fmt.Printf("Received: %d, open: %t\n", n, open)
}

运行示例

在上面的程序中,我们创建了一个容量为 5 的缓冲通道。 然后我们将 5 和 6 写入通道。 接着通道关闭。即使通道关闭,我们也可以读取已经写入通道的值。 n 的值将是 5 并且 open 在为真。 下一个 n 的值将是 6 并且 open 依然为真。我们现在已经完成了从通道中读取 5 和 6 ,接下来没有更多数据要读取。 现在,当再次从通道读取数据时。n 的值将为 0,即 int 的零值,open 将为假,此时表示通道已关闭。

Go 关闭通道

下面我们可以通过使用 for range 来重写上面的程序

package main

import (  
    "fmt"
)

func main() {  
    ch := make(chan int, 5)
    ch <- 5
    ch <- 6
    close(ch)
    for n := range ch {
        fmt.Println("Received:", n)
    }
}

运行示例

上面程序中使用 for range 循环来读取写入通道的所有值,并且一旦由于通道已经关闭而没有更多值要读取时循环将退出。

程序执行结果如下

go 关闭通道示例2

除非注明转载,本站文章均为原创或翻译,欢迎转载,转载请以链接形式注明出处

本文地址:

迹忆客

专注技术分享,项目实战分享!

技术宅 乐于分享 7年编程经验
社交账号
  • https://www.github.com/onmpw
  • qq:1244347461

热门文章

教程更新

热门标签

Go