我有一个场景,我正在通道上处理事件,其中一个事件是需要在特定时间范围内发生的心跳。非心跳的事件将继续消耗计时器,但是每当收到心跳时我想重置计时器。执行此操作的明显方法是使用 time.NewTimer.
例如:
\nfunc main() {\n to := time.NewTimer(3200 * time.Millisecond)\n for {\n select {\n case event, ok := <-c:\n if !ok {\n return\n } else if event.Msg == "heartbeat" {\n to.Reset(3200 * time.Millisecond) \n }\n case remediate := <-to.C:\n fmt.Println("do some stuff ...")\n return\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n请注意,time.Ticker此处不起作用,因为只有在未收到心跳的情况下才应触发修复,而不是每次都如此。
上面的解决方案在我尝试过的少数低容量测试中有效,但是我遇到了一个 Github 问题,表明重置尚未触发的计时器是不行的。此外,文档还指出:
\n\n\n仅应在通道已耗尽的计时器停止或过期时调用重置。如果程序已经从 tC 接收到一个值,则知道定时器已到期并且通道已耗尽,因此可以直接使用 t.Reset。但是,如果程序尚未从 tC 接收到值,则必须停止计时器,并且\xe2\x80\x94如果 Stop 报告计时器在停止之前已过期\xe2\x80\x94通道显式耗尽:
\n
if !t.Stop() {\n <-t.C\n}\nt.Reset(d)\nRun Code Online (Sandbox Code Playgroud)\n这让我停了下来,因为它似乎准确地描述了我正在尝试做的事情。每当收到心跳时,我就会Timer在触发之前重置 。我对 Go 的经验还不够,还没有消化整篇文章,但看起来我可能会走上一条危险的道路。
我想到的另一种解决方案是Timer每当心跳发生时简单地用新的替换,例如:
else if event.Msg == "heartbeat" {\n to = time.NewTimer(3200 * time.Millisecond) \n }\nRun Code Online (Sandbox Code Playgroud)\n起初我担心重新绑定to = time.NewTimer(3200 * time.Millisecond) 在选择中不可见:
\n\n对于语句中的所有情况,接收操作的通道操作数以及发送语句的通道和右侧表达式在输入“select”语句时按源顺序仅计算一次。结果是一组要接收或发送到的通道,以及要发送的相应值。
\n
但在这种特殊情况下,由于我们处于循环内,我希望在每次迭代时我们重新输入 select ,因此新的绑定应该是可见的。这是一个公平的假设吗?
\n我意识到那里有类似的问题,并且我尝试阅读相关的帖子/文档,但我是 Go 新手,只是想确保我在这里正确理解了事情。
\n所以我的问题是:
\n我的使用是否timer.Reset()不安全,或者 Github 问题中提到的案例突出显示了此处不适用的其他问题?文档中的解释是否晦涩难懂,还是我只是需要更多 Go 经验?
如果不安全,我提出的第二个解决方案是否可以接受(在每次迭代时重新绑定计时器)。
\n附录
!t.Stop(),因为 Stop 的错误返回将表明计时器已经触发,因此必须在调用 Reset 之前耗尽。\n我仍然不明白的是,为什么有必要在尚未开火时t.Stop()先打电话。据我所知,没有一个例子涉及到这一点。t.Reset()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")
}
}
}
Run Code Online (Sandbox Code Playgroud)
在操场上尝试一下。
这可能看起来是人为的,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)
Run Code Online (Sandbox Code Playgroud)
在操场上试试这个。
这是有效的,因为“如果调用停止计时器,to.Stop 则返回true;如果计时器已过期或已停止,则返回 false”。请注意,如果在多个 go 例程中使用计时器,事情会变得更加复杂“这不能与来自计时器通道的其他接收或对计时器停止方法的其他调用同时完成”,但在您的用例中情况并非如此。
我对timer.Reset()的使用是否不安全,或者Github问题中提到的情况是否强调了此处不适用的其他问题?
是的 - 这是不安全的。但影响相当低。事件到达和计时器触发需要几乎同时发生,在这种情况下,运行代码remediate可能不是一个大问题。请注意,修复相当简单(根据文档)
如果不安全,我提出的第二个解决方案是否可以接受(在每次迭代时重新绑定计时器)。
您提出的第二个解决方案也有效(但请注意,垃圾收集器在触发或停止之前无法释放计时器,如果您快速创建计时器,这可能会导致问题)。
注意:回复 @JotaSantos 的建议
另一件可以做的事情是在使用默认子句排除 <-to.C (在 Stop“if”上)时添加一个选择。这将防止暂停。
请参阅此评论,了解为什么这可能不是一个好方法的详细信息(在您的情况下也没有必要)。
| 归档时间: |
|
| 查看次数: |
4094 次 |
| 最近记录: |