select组中指定时间内没有频道收到信号时跳出循环

Break out of loop when no channel in a select group receives signal in specified time

当且仅当我在 select 语句正在收听的任何频道上都没有收到任何信号时,我该如何跳出包含 select 语句的惯用 Go for 循环,对于一个特定时期。

让我用一个例子来加强这个问题。

设置:

  1. 假设我有一个正在收听的频道 var listenCh <-chan string
  2. 让我们假设一些其他的 go 例程(不在我们的控制范围内)在此通道上发送不同的字符串。
  3. 我对给定的字符串进行一些处理,然后在 listenCh 上监听下一个字符串。

要求:

我想在 listenCh 上的两个连续信号之间最多等待 10 秒(精度不重要),然后再关闭我的操作(永久中断 for 循环)。

代码存根:

func doingSomething(listenCh <-chan string) {
  var mystr string
  for {
    select {
      case mystr <-listenCh:
        //dosomething
      case /*more than 10 seconds since last signal on listenCh*/:
        return
    }
  }
}

我将如何以最有效的方式实现我的要求。

通常使用 time.After(time.Duration) 的退出通道技术似乎不会在一个循环后重置,因此即使有连续的值流,整个程序也会在 10 秒内关闭。

我在 SO 上找到了问题的变体(但不是我想要的),但是我看到的 none 回答了我的特定用例。

前言:推荐使用time.Timer,此处使用time.After()仅用于演示和推理。请使用第二种方法。


使用 time.After()(不推荐这样做)

如果你把 time.After() 放在 case 分支中,那将在每次迭代中 "reset",因为每次都会 return 你一个新的频道,这样就可以了:

func doingSomething(listenCh <-chan string) {
    for {
        select {
        case mystr := <-listenCh:
            log.Println("Received", mystr)
        case <-time.After(1 * time.Second):
            log.Println("Timeout")
            return
        }
    }
}

(我在 Go Playground 上使用了 1 秒的可测试性超时。)

我们可以这样测试:

ch := make(chan string)
go func() {
    for i := 0; i < 3; i++ {
        ch <- fmt.Sprint(i)
        time.Sleep(500 * time.Millisecond)
    }
}()
doingSomething(ch)

输出(在 Go Playground 上尝试):

2009/11/10 23:00:00 Received 0
2009/11/10 23:00:00 Received 1
2009/11/10 23:00:01 Received 2
2009/11/10 23:00:02 Timeout

使用time.Timer(推荐的解决方案)

如果从频道接收的速率很高,这可能会有点浪费资源,因为 time.After() 在后台创建并使用了一个新的计时器,它不会神奇地停止并且如果您在超时前从通道收到值,则在不再需要时立即收集垃圾。

一种资源更友好的解决方案是在循环之前创建一个 time.Timer,如果在超时之前收到一个值,则将其重置。

这是它的样子:

func doingSomething(listenCh <-chan string) {
    d := 1 * time.Second
    t := time.NewTimer(d)
    for {
        select {
        case mystr := <-listenCh:
            log.Println("Received", mystr)
            if !t.Stop() {
                <-t.C
            }
            t.Reset(d)
        case <-t.C:
            log.Println("Timeout")
            return
        }
    }
}

测试和输出是一样的。在 Go Playground.

上试试这个