去重置一个计时器.选择循环中的新计时器



我有一个场景,其中我正在处理通道上的事件,其中一个事件是需要在某个时间范围内发生的心跳。不是心跳的事件将继续消耗计时器,但是每当收到检测信号时,我都想重置计时器。执行此操作的明显方法是使用time.NewTimer.

例如:

func main() {
to := time.NewTimer(3200 * time.Millisecond)
for {
select {
case event, ok := <-c:
if !ok {
return
} else if event.Msg == "heartbeat" {
to.Reset(3200 * time.Millisecond)               
}
case remediate := <-to.C:
fmt.Println("do some stuff ...")
return
}
}
}

请注意,time.Ticker在此处不起作用,因为只有在未收到检测信号时才应触发修正,而不是每次都触发。

上述解决方案适用于我尝试过的少数少量测试,但是我遇到了一个 Github 问题,表明重置未触发的计时器是不行的。此外,文档还指出:

重置应仅在通道耗尽的已停止或过期计时器上调用。如果程序已经从 t.C 接收到值,则已知计时器已过期并且通道已耗尽,因此可以直接使用 t.Reset 。但是,如果程序尚未从 t.C 接收值,则必须停止计时器,如果 Stop 报告计时器在停止之前已过期,则通道显式耗尽:

if !t.Stop() {
<-t.C
}
t.Reset(d)

这让我停顿了一下,因为它似乎准确地描述了我试图做的事情。每当收到心跳时,在触发之前,我都会重置Timer。我对 Go 还没有足够的经验来消化整篇文章,但看起来我可能正在走上一条危险的道路。

我想到的另一个解决方案是在发生心跳时简单地用新的Timer替换,例如:

else if event.Msg == "heartbeat" {
to = time.NewTimer(3200 * time.Millisecond)               
}

起初我担心重新绑定to = time.NewTimer(3200 * time.Millisecond)在选择中不可见:

对于语句中的所有情况,在输入 "select" 语句时,接收操作的通道操作数以及 send 语句的通道和右侧表达式将按源顺序精确计算一次。结果是一组要接收或发送到的通道,以及要发送的相应值。

但是在这种特殊情况下,由于我们处于循环内,我希望在每次迭代时我们都会重新输入 select,因此新绑定应该是可见的。 这是一个公平的假设吗?

我意识到那里有类似的问题,并且我尝试阅读相关的帖子/文档,但我是Go的新手,只是想确保我在这里正确理解了事情。

所以我的问题是:

  • 我使用timer.Reset()是不安全的,还是 Github 问题中提到的情况突出了此处不适用的其他问题?文档中的解释是神秘的,还是我只需要更多的 Go 经验?

  • 如果不安全,我的第二个建议的解决方案是否可以接受(在每次迭代时重新绑定计时器)。

附录


进一步阅读后,问题中概述的大多数陷阱都描述了计时器已经触发(将结果放在通道上)的场景,并且在触发之后,其他一些进程尝试重置它。对于这种狭窄的情况,我理解需要用!t.Stop()进行测试,因为错误返回 Stop 将表明计时器已经触发,因此必须在调用 Rereset 之前耗尽。

我仍然不明白的是,为什么有必要在t.Reset()之前打电话给t.Stop(),当Timer尚未开火时。 据我所知,没有一个例子涉及这一点。

我仍然不明白的是,为什么在 t.Reset() 之前调用 t.Stop(),当计时器尚未触发时。

"当计时器尚未触发时"位在这里至关重要。计时器在单独的 go 例程(运行时的一部分)中触发,这可能随时发生。您无法知道计时器在您调用to.Reset(3200 * time.Millisecond)时是否已触发(它甚至可能在该函数运行时触发!

下面是一个演示这一点的示例,它与您正在尝试的内容有些相似(基于此):


func main() {
eventC := make(chan struct{}, 1)
go keepaliveLoop(eventC )
// Reset the timer 1000 (approx) times; once every millisecond (approx)
// This should prevent the timer from firing (because that only happens after 2 ms)
for i := 0; i < 1000; i++ {
time.Sleep(time.Millisecond)
// Don't block if there is already a reset request
select {
case eventC <- struct{}{}:
default:
}
}
}
func keepaliveLoop(eventC chan struct{}) {
to := time.NewTimer(2 * time.Millisecond)
for {
select {
case <-eventC: 
//if event.Msg == "heartbeat"...
time.Sleep(3 * time.Millisecond) // Simulate reset work (delay could be partly dur to whatever is triggering the
to.Reset(2 * time.Millisecond)
case <-to.C:
panic("this should never happen")
}
}
}

在操场上试试。

由于time.Sleep(3 * time.Millisecond),这可能看起来是人为的,但这只是为了一致地展示问题。您的代码可能在 99.9% 的时间内工作,但事件和计时器通道始终有可能在运行select之前(其中将运行随机情况)或在case event, ok := <-c:块中的代码运行时(包括Reset()正在进行时)触发。发生这种情况的结果将是意外调用remediate代码(这可能不是一个大问题)。

幸运的是,解决问题相对容易(遵循文档中的建议):

time.Sleep(3 * time.Millisecond) // Simulate reset work (delay could be partly dur to whatever is triggering the
if !to.Stop() {
<-to.C
}
to.Reset(2 * time.Millisecond)

在操场上试试这个。

这是有效的to.Stop因为如果调用停止计时器,则返回 true,如果计时器已经过期或已停止,则返回 false"。请注意,如果计时器用于多个 go-routine,事情会变得更加复杂"这不能与来自计时器通道的其他接收或对计时器停止方法的其他调用同时完成",但在您的用例中并非如此。

是我使用计时器。Reset() 不安全,还是 Github 问题中提到的情况突出了此处不适用的其他问题?

是的 - 这是不安全的。然而,影响相当小。事件到达和计时器触发几乎需要同时发生,在这种情况下,运行remediate代码可能不是一个大问题。请注意,修复程序相当简单(根据文档)

如果不安全,我的第二个建议的解决方案是否可以接受(在每次迭代时重新绑定计时器)。

您建议的第二个解决方案也可以工作(但请注意,垃圾回收器在触发或停止之前无法释放计时器,如果您快速创建计时器,这可能会导致问题)。

注意:重新建议@JotaSantos

可以做的另一件事是在排空 <时添加一个选择。C(在停止"if")上,带有默认子句。这将防止停顿。>

有关为什么这可能不是一个好方法的详细信息,请参阅此评论(在您的情况下也没有必要)。

我遇到了类似的问题。在阅读了大量信息后,我想出了一个解决方案,大致如下:

package main
import (
"fmt"
"time"
)
func main() {
const timeout = 2 * time.Second
// Prepare a timer that is stopped and ready to be reset.
// Stop will never return false, because an hour is too long
// for timer to fire. Thus there's no need to drain timer.C.
timer := time.NewTimer(timeout)
timer.Stop()
// Make sure to stop the timer when we return.
defer timer.Stop()
// This variable is needed because we need to track if we can safely reset the timer
// in a loop. Calling timer.Stop() will return false on every iteration, but we can only
// drain the timer.C once, otherwise it will deadlock.
var timerSet bool
c := make(chan time.Time)
// Simulate events that come in every second
// and every 5th event delays so that timer can fire.
go func() {
var i int
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for t := range ticker.C {
i++
if i%5 == 0 {
fmt.Println("Sleeping")
time.Sleep(3 * time.Second)
}
c <- t
if i == 20 {
break
}
}
close(c)
}()
for {
select {
case t, ok := <-c:
if !ok {
fmt.Println("Closed channel")
return
}
fmt.Println("Got event", t, timerSet)
// We got an event, and timer was already set.
// We need to stop the timer and drain the channel if needed,
// so that we can safely reset it later.
if timerSet {
if !timer.Stop() {
<-timer.C
}
timerSet = false
}
// If timer was not set, or it was stopped before, it's safe to reset it.
if !timerSet {
timerSet = true
timer.Reset(timeout)
}
case remediate := <-timer.C:
fmt.Println("Timeout", remediate)
// It's important to store that timer is not set anymore.
timerSet = false
}
}
}

链接到游乐场:https://play.golang.org/p/0QlujZngEGg

最新更新