"Select" goroutine 内部的 for 循环语句

"Select" statement inside of goroutine with for loop

谁能解释一下,为什么goroutine有无穷无尽的for循环,循环里面有select,循环里的一段代码只有运行一次?

package main

import (
    "time"
)

func f1(quit chan bool){
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            select{
            case <-quit:
            println("stopping f1")
            break
            }
        }   
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
}

输出:

f1 is working...
Program exited.

但是如果"select"被注释掉了:

package main

import (
    "time"
)

func f1(quit chan bool){
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            //select{
            //case <-quit:
            //println("stopping f1")
            //break
            //}
        }   
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
}

输出:

f1 is working...
f1 is working...
f1 is working...
f1 is working...
f1 is working...

https://play.golang.org/p/MxKy2XqQlt8

没有 default case 的 select 语句是 阻塞 直到 case 语句中的至少一个读或写可以被执行。因此,您的 select 将阻塞,直到可以从 quit 通道读取数据(如果通道关闭,则为值或零值)。语言规范提供了此行为的 concrete description,特别是:

If one or more of the communications [expressed in the case statements] can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.

其他代码问题

注意! break 适用于 select 语句

但是,即使您确实关闭了 quit 通道以发出关闭程序的信号,您的实施也可能不会产生预期的效果。对 break 的(未标记的)调用将 terminate execution of the inner-most for, select or switch statement within the function. In this case, the select statement will break and the for loop will run through again. If quit was closed, it will run forever until something else stops the program, otherwise it will block again in select (Playground example)

关闭 quit 将(可能)不会立即停止程序

如前所述 ,对 time.Sleep 的调用在循环的每次迭代中阻塞一秒钟,因此任何通过关闭 quit 来停止程序的尝试都会延迟在 goroutine 检查 quit 并转义之前大约一秒。在程序停止之前,这个睡眠周期不太可能必须完全完成。

更多地道的 Go 会在 select 从两个通道接收的语句中阻塞:

  • quit频道
  • time.After – this call is an abstraction around a Timer返回的通道休眠一段时间,然后将值写入提供的通道。

分辨率

修改最少的解决方案

通过对代码进行最少更改来解决您眼前的问题的解决方案是:

  • 通过向 select 语句添加默认情况,使从 quit 的读取成为非阻塞。
  • 当从 quit 读取成功时,确保 goroutine 实际上 returns:
    • 标记 for 循环并使用标记调用 break;或
    • returnf1 函数退出(首选)

根据您的情况,您可能会发现使用 context.Context to signal termination, and to use a sync.WaitGroup 在从 main.

返回之前等待 goroutine 完成更为惯用
package main

import (
    "fmt"
    "time"
)

func f1(quit chan bool) {
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            select {
            case <-quit:
                fmt.Println("stopping")
                return
            default:
            }
        }
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
    close(quit)
    time.Sleep(4 * time.Second)
}

Working example

(注意:我在您的 main 方法中添加了一个额外的 time.Sleep 调用,以避免在调用 close 并终止程序后立即返回。)

解决睡眠阻塞问题

要解决有关阻止立即退出的阻塞睡眠的其他问题,请将睡眠移动到 select 块中的计时器。根据评论中的 playground example 修改 for 循环正是这样做的:

for {
    println("f1 is working...")

    select {
    case <-quit:
        println("stopping f1")
        return
    case <-time.After(1 * time.Second):
        // repeats loop
    }
}

相关文献