zoukankan      html  css  js  c++  java
  • Go语言通道(chan)——goroutine之间通信的管道

    Go语言通道(chan)——goroutine之间通信的管道

    如果说 goroutine 是 Go语言程序的并发体的话,那么 channels 就是它们之间的通信机制。一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。

    Go语言提倡使用通信的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

    这里通信的方法就是使用通道(channel),如下图所示。


    图:goroutine 与 channel 的通信


    在地铁站、食堂、洗手间等公共场所人很多的情况下,大家养成了排队的习惯,目的也是避免拥挤、插队导致的低效的资源使用和交换过程。代码与数据也是如此,多个 goroutine 为了争抢数据,势必造成执行的低效率,使用队列的方式是最高效的,channel 就是一种队列一样的结构。

    通道的特性

    Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。

    通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

    声明通道类型

    通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:

    var 通道变量 chan 通道类型

    • 通道类型:通道内的数据类型。
    • 通道变量:保存通道的变量。


    chan 类型的空值是 nil,声明后需要配合 make 后才能使用。

    创建通道

    通道是引用类型,需要使用 make 进行创建,格式如下:

    通道实例 := make(chan 数据类型)

    • 数据类型:通道内传输的元素类型。
    • 通道实例:通过make创建的通道句柄。


    请看下面的例子:

    1. ch1 := make(chan int) // 创建一个整型类型的通道
    2. ch2 := make(chan interface{}) // 创建一个空接口类型的通道, 可以存放任意格式
    3. type Equip struct{ /* 一些字段 */ }
    4. ch2 := make(chan *Equip) // 创建Equip指针类型的通道, 可以存放*Equip

    使用通道发送数据

    通道创建后,就可以使用通道进行发送和接收操作。

    1) 通道发送数据的格式

    通道的发送使用特殊的操作符<-,将数据通过通道发送的格式为:

    通道变量 <- 值

    • 通道变量:通过make创建好的通道实例。
    • 值:可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。

    2) 通过通道发送数据的例子

    使用 make 创建一个通道后,就可以使用<-向通道发送数据,代码如下:

    1. // 创建一个空接口通道
    2. ch := make(chan interface{})
    3. // 将0放入通道中
    4. ch <- 0
    5. // 将hello字符串放入通道中
    6. ch <- "hello"

    3) 发送将持续阻塞直到数据被接收

    把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go 程序运行时能智能地发现一些永远无法发送成功的语句并做出提示,代码如下:

    1. package main
    2. func main() {
    3. // 创建一个整型通道
    4. ch := make(chan int)
    5. // 尝试将0通过通道发送
    6. ch <- 0
    7. }

    运行代码,报错:

    fatal error: all goroutines are asleep - deadlock!

    报错的意思是:运行时发现所有的 goroutine(包括main)都处于等待 goroutine。也就是说所有 goroutine 中的 channel 并没有形成发送和接收对应的代码。

    使用通道接收数据

    通道接收同样使用<-操作符,通道接收有如下特性:
    ① 通道的收发操作在不同的两个 goroutine 间进行。

    由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个 goroutine 中进行。

    ② 接收将持续阻塞直到发送方发送数据。

    如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。

    ③ 每次接收一个元素。
    通道一次只能接收一个数据元素。

    通道的数据接收一共有以下 4 种写法。

    1) 阻塞接收数据

    阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:

    data := <-ch

    执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。

    2) 非阻塞接收数据

    使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:

    data, ok := <-ch

    • data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
    • ok:表示是否接收到数据。


    非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行,可以参见后面的内容。

    3) 接收任意数据,忽略接收的数据

    阻塞接收数据后,忽略从通道返回的数据,格式如下:

    <-ch

    执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。

    使用通道做并发同步的写法,可以参考下面的例子:

    1. package main
    2. import (
    3. "fmt"
    4. )
    5. func main() {
    6. // 构建一个通道
    7. ch := make(chan int)
    8. // 开启一个并发匿名函数
    9. go func() {
    10. fmt.Println("start goroutine")
    11. // 通过通道通知main的goroutine
    12. ch <- 0
    13. fmt.Println("exit goroutine")
    14. }()
    15. fmt.Println("wait goroutine")
    16. // 等待匿名goroutine
    17. <-ch
    18. fmt.Println("all done")
    19. }

    执行代码,输出如下:

    wait goroutine
    start goroutine
    exit goroutine
    all done

    代码说明如下:

    • 第 10 行,构建一个同步用的通道。
    • 第 13 行,开启一个匿名函数的并发。
    • 第 18 行,匿名 goroutine 即将结束时,通过通道通知 main 的 goroutine,这一句会一直阻塞直到 main 的 goroutine 接收为止。
    • 第 27 行,开启 goroutine 后,马上通过通道等待匿名 goroutine 结束。

    4) 循环接收

    通道的数据接收可以借用 for range 语句进行多个元素的接收操作,格式如下:

    1. for data := range ch {
    2. }

    通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。

    遍历通道数据的例子请参考下面的代码。

    使用 for 从通道中接收数据:

    1. package main
    2. import (
    3. "fmt"
    4. "time"
    5. )
    6. func main() {
    7. // 构建一个通道
    8. ch := make(chan int)
    9. // 开启一个并发匿名函数
    10. go func() {
    11. // 从3循环到0
    12. for i := 3; i >= 0; i-- {
    13. // 发送3到0之间的数值
    14. ch <- i
    15. // 每次发送完时等待
    16. time.Sleep(time.Second)
    17. }
    18. }()
    19. // 遍历接收通道数据
    20. for data := range ch {
    21. // 打印通道数据
    22. fmt.Println(data)
    23. // 当遇到数据0时, 退出接收循环
    24. if data == 0 {
    25. break
    26. }
    27. }
    28. }

    执行代码,输出如下:

    3
    2
    1
    0

    代码说明如下:

      • 第 12 行,通过 make 生成一个整型元素的通道。
      • 第 15 行,将匿名函数并发执行。
      • 第 18 行,用循环生成 3 到 0 之间的数值。
      • 第 21 行,将 3 到 0 之间的数值依次发送到通道 ch 中。
      • 第 24 行,每次发送后暂停 1 秒。
      • 第 30 行,使用 for 从通道中接收数据。
      • 第 33 行,将接收到的数据打印出来。
      • 第 36 行,当接收到数值 0 时,停止接收。如果继续发送,由于接收 goroutine 已经退出,没有 goroutine 发送到通道,因此运行时将会触发宕机报错。
  • 相关阅读:
    【原创】Zend Framework快速开发(二)使用命令完成项目
    分析CMMS系统笔记使用js控制快捷键
    学习笔记——php利用@来抑制错误
    WINDOWS + WAMP + Zend Framework 配置
    PHP的$_SERVER变量笔记
    【原创】Zend Framework快速开发(一)zend框架的配置和项目创建(原创)
    带符号的8位2进制数最小值为多少?
    动态规划笔记
    我一直在拖国家的后退
    手机客户端和服务器通信时如何安全高效的进行身份验证
  • 原文地址:https://www.cnblogs.com/show58/p/12611466.html
Copyright © 2011-2022 走看看