用nil channel解决关闭的channel输出0的问题

用nil channel解决关闭的channel输出0的问题

Content #

func main() {
    ch1, ch2 := make(chan int), make(chan int)
    go func() {
        time.Sleep(time.Second * 5)
        ch1 <- 5
        close(ch1)
    }()

    go func() {
        time.Sleep(time.Second * 7)
        ch2 <- 7
        close(ch2)
    }()

    var ok1, ok2 bool
    for {
        select {
        case x := <-ch1:
            ok1 = true
            fmt.Println(x)
        case x := <-ch2:
            ok2 = true
            fmt.Println(x)
        }

        if ok1 && ok2 {
            break
        }
    }
    fmt.Println("program end")
}

我们期望程序在接收完 ch1 和 ch2 两个 channel 上的数据后就退出。但实际的运行情况却是这样的:

5
0
0
0
... ... //循环输出0
7
program end

这是怎么回事呢?我们简单分析一下这段代码的运行过程:

  1. 前 5s,select 一直处于阻塞状态;
  2. 第 5s,ch1 返回一个 5 后被 close,select 语句的case x := <-ch1这个分支被选出执行,程序输出 5,并回到 for 循环并重新 select;
  3. 由于 ch1 被关闭,从一个已关闭的 channel 接收数据将永远不会被阻塞,于是新一轮 select 又把case x := <-ch1这个分支选出并执行。由于 ch1 处于关闭状态,从这个 channel 获取数据,我们会得到这个 channel 对应类型的零值,这里就是 0。于是程序再次输出 0;程序按这个逻辑循环执行,一直输出 0 值;
  4. 2s 后,ch2 被写入了一个数值 7。这样在某一轮 select 的过程中,分支 case x := <-ch2被选中得以执行,程序输出 7 之后满足退出条件,于是程序终止。

用 nil channel 改进后的示例代码是这样的:

func main() {
    ch1, ch2 := make(chan int), make(chan int)
    go func() {
        time.Sleep(time.Second * 5)
        ch1 <- 5
        close(ch1)
    }()

    go func() {
        time.Sleep(time.Second * 7)
        ch2 <- 7
        close(ch2)
    }()

    for {
        select {
        case x, ok := <-ch1:
            if !ok {
                ch1 = nil
            } else {
                fmt.Println(x)
            }
        case x, ok := <-ch2:
            if !ok {
                ch2 = nil
            } else {
                fmt.Println(x)
            }
        }
        if ch1 == nil && ch2 == nil {
            break
        }
    }
    fmt.Println("program end")
}

改进后的示例程序的最关键的一个变化,就是在判断 ch1 或 ch2 被关闭后,显式地将 ch1 或 ch2 置为 nil。

而我们前面已经知道了,对一个 nil channel 执行获取操作,这个操作将阻塞。于是,这里已经被置为 nil 的 c1 或 c2 的分支,将再也不会被 select 选中执行。

改进后的示例的运行结果如下,与我们预期相符:

5
7
program end

Viewpoint #

From #

33|并发:小channel中蕴含大智慧