(12-3-01)并发编程:通道(channel)(1)

12.3  通道(channel)

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

12.3.1  通道介绍

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

图12-1  通道(channel)

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

通道的内部实现依赖于Go语言的调度器和运行时系统。当一个goroutine执行发送或接收操作时,如果通道没有准备好,那么该goroutine会被阻塞,直到通道准备好为止。此时,调度器会自动切换到其他goroutine,使得程序可以充分利用计算机的多核处理能力。同时,通道还具有同步机制,即发送和接收操作都是"原子性"的,任何时刻只有一个goroutine可以进行操作。这避免了竞态条件和死锁等问题,并保证了程序的正确性和可靠性。

12.3.2  创建通道

在Go语言中,和通道相关的函数如下:

  1. 函数make():用于创建一个通道对象,可以指定通道的类型和缓冲容量。
  2. 函数close():用于关闭一个通道对象,表示该通道不再进行数据传输。对于已经关闭的通道,发送操作会报错,接收操作则会返回通道的零值。
  3. 函数len():用于获取一个通道中当前存储的元素数量。
  4. 函数cap():用于获取一个通道的缓冲容量,如果通道没有缓冲,则返回0。

在Go语言中,使用函数make()创建一个通道实例,具体语法格式如下:

通道实例 := make(chan 数据类型)
  1. 数据类型:通道内传输的元素类型。
  2. 通道实例:通过make创建的通道句柄。

例如下面的例子:

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

12.3.3  使用通道发送数据

在通道创建后,接下来就可以使用通道实现数据的发送和接收操作。其中使用通道发送数据的语法格式如下:

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

在使用函数make()创建一个通道后,就可以使用<-向通道发送数据,例如下面的演示代码:

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

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

package main

func main() {

    // 创建一个整型通道

    ch := make(chan int)

    // 尝试将0通过通道发送

    ch <- 0

}

运行上述代码时会报错:

fatal error: all goroutines are asleep - deadlock!

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

实例12-3:使用通道在不同的goroutine之间传递数据(源码路径:Go-codes\12\fa.go

实例文件fa.go的具体实现代码如下所示。

import "fmt"

func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

func main() {
    ch := make(chan int)
    go producer(ch)
    for num := range ch {
        fmt.Printf("Received %d\n", num)
    }
}

对上述代码的具体说明如下:

  1. 首先定义了一个名为producer()的函数,用于生成一定数量的数字,并将它们写入通道ch中。注意,这里使用了单向通道(chan<- int),表示该通道仅用于发送数据。
  2. 在函数main()中创建了通道ch,然后使用关键字"go"和函数producer()调用来创建并启动一个goroutine,从而实现了并发执行任务。
  3. 接着,使用range循环遍历通道ch,以便接收所有产生的数字。

执行后会输出:

Received 0
Received 1
Received 2
Received 3
Received 4

注意:在上述例子中,我们使用了通道机制来进行goroutine之间的数据传递,但没有使用通道的同步机制。由于通道是阻塞的,当通道中没有可读取的数据时,接收操作会被自动阻塞,直到有新的数据可以读取为止。因此,在这个例子中,我们可以通过遍历通道来获取所有产生的数字,而无需进行额外的同步操作。

12.3.4  使用通道接收数据

在Go语言中,通道接收功能同样使用操作符“<-”实现,通道接收有如下3个特性:

(1)通道的收发操作在不同的两个 goroutine 间进行:由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个 goroutine 中进行。

(2)接收将持续阻塞直到发送方发送数据:如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。

(3)每次接收一个元素:通道一次只能接收一个数据元素。

在Go语言中,使用通道接收数据的方法一共有以下 4 种写法。

1. 阻塞接收数据

当使用阻塞模式接收数据时,将接收变量作为操作符“<-”的左值,具体格式如下:

data := <-ch

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

2. 非阻塞接收数据

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

data, ok := <-ch
  1. data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
  2. ok:表示是否接收到数据。

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

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

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

<-ch

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

4. 循环接收

在使用通道接收数据时,还可以借用 for range 语句接收多个元素,具体格式如下:

for data := range ch {
}

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

请看下面的实例,模拟实现了一个简单的游戏程序。在这个游戏中,每个玩家都会进行一定的操作,并根据随机生成的分数来计算得分。通道用于记录每个玩家的得分,并将它们传递给主程序,最后通过计算得出总得分。

实例12-4:文件的打开与关闭(源码路径:Go-codes\12\game.go

实例文件game.go的具体实现代码如下所示。

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

func playGame(player string, ch chan int) {
    fmt.Printf("%s is ready to play!\n", player)
    time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
    score := rand.Intn(100)
    fmt.Printf("%s got a score of %d!\n", player, score)
    ch <- score
}

func main() {
    players := []string{"Alice", "Bob", "Charlie", "David", "Eve"}
    scores := make(chan int, len(players))

    for _, player := range players {
        go playGame(player, scores)
    }

    totalScore := 0
    for i := 0; i < len(players); i++ {
        score := <-scores
        totalScore += score
    }

    fmt.Printf("Total score: %d\n", totalScore)
}

对上述代码的具体说明如下:

  1. 首先定义了一个名为playGame()的函数来模拟游戏过程。
  2. 然后,创建了一个通道scores,并使用关键字"go"和playGame()函数调用来创建并启动多个goroutine,每个goroutine代表一个玩家,从而实现了并发执行游戏任务。
  3. 在每次游戏结束后,函数playGame()会将该玩家的得分写入通道scores中。
  4. 在main()函数中,通过遍历scores通道来收集所有玩家的得分,并计算出总得分。
  5. 最后,打印输出总得分。

由于每个玩家的得分都是随机生成的,所以每次运行程序时,输出结果都可能会略有不同。执行后会输出:

Eve is ready to play!
Alice is ready to play!
Bob is ready to play!
Charlie is ready to play!
Charlie got a score of 64!
David is ready to play!
David got a score of 69!
Bob got a score of 31!
Eve got a score of 44!
Alice got a score of 96!
Total score: 304

注意:在上述例子中,使用了通道机制来进行goroutine之间的数据同步和数据传递。在函数main()中,当所有玩家的得分都写入到scores通道中后,可以通过遍历scores通道的方式来收集所有得分,并保证它们的顺序与原始玩家列表的顺序一致。

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值