8.7. channel通道的多路复用

多路复用是通信和网络中的一个专业术语。多路复用通常表示在一个信道上传输多路信号或数据流的过程和技术。

提示 报话机同一时刻只能有一边进行收或者发的单边通信,报话机需要遵守的通信流程如下:

  • 说话方在完成时需要补上一句“完毕”,随后放开通话按钮,从发送切换到接收状态,收听对方说话。

  • 收听方在听到对方说“完毕”时,按下通话按钮,从接收切换到发送状态,开始说话。

电话可以在说话的同时听到对方说话,所以电话是一种多路复用的设备,一条通信线路上可以同时接收或者发送数据。 同样的,网线、光纤也都是基于多路复用模式来设计的,网线、光纤不仅可支持同时收发数据,还支持多个人同时收发数据。

在使用通道时,想同时接收多个通道的数据是一件困难的事情。

通道在接收数据时,如果没有数据可以接收将会发生阻塞。

虽然可以使用如下模式进行遍历,但运行性能会非常差。

for{
    // 尝试接收ch1通道
    data, ok := <-ch1

    // 尝试接收ch2通道
    data, ok := <-ch2

    // 接收后续通道
    …
}

Go语言中提供了select关键字,可以同时响应多个通道的操作。select 用法与switch语句非常类似,由select开始一个新的选择块, 每个选择条件由case语句来描述。

与switch语句可以选择任何可使用相等比较的条件相比,select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作, 大致结构如下:

select{
    case 操作1:
        响应操作1

    case 操作2:
        响应操作2
    …
    default:
        没有操作情况
}
  • 操作1、操作2:包含通道收发语句,请参考下表。

select多路复用中可以接收的样式

操作

语句示例

接收任意数据

case <- ch;

接收变量

case d:= <- ch;

发送数据

case ch <- 100;

  • 响应操作1、响应操作2:当操作发生时,会执行对应case的响应操作。

  • default:当没有任何操作时,默认执行default中的语句。

可以看出,select不像switch,后面并不带判断条件,而是直接去查看case语句。

每个case语句都必须是一个面向channel的操作。

package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    for i := 1; i <= 10; i++ {
        select {
        case x := <-ch:
            fmt.Println(x)
        case ch <- i:
        }
    }
}

上面的代码输出内容如下。

1
3
5
7
9

示例中的代码首先是创建了一个缓冲区大小为1的通道 ch,进入 for 循环后:

  • 第一次循环时 i = 1,select 语句中包含两个 case 分支,此时由于通道中没有值可以接收,所以x := <-ch 这个 case 分支不满足,而ch <- i这个分支可以执行,会把1发送到通道中,结束本次 for 循环;

  • 第二次 for 循环时,i = 2,由于通道缓冲区已满,所以ch <- i这个分支不满足,而x := <-ch这个分支可以执行,从通道接收值1并赋值给变量 x ,所以会在终端打印出 1;

  • 后续的 for 循环以此类推会依次打印出3、5、7、9。

Select 语句具有以下特点。

  • 可处理一个或多个 channel 的发送/接收操作。

  • 如果多个 case 同时满足,select 会随机选择一个执行。

  • 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。