如何停止正在另一个go协程中监听的time.Timer?

7
我有一个空闲超时计时器在goroutine中被select,在看到活动时,我想取消计时器。
我查看了文档,但不确定是否理解正确。
Stop函数可以阻止计时器触发。如果调用成功停止计时器,则返回true;如果计时器已经过期或已停止,则返回false。Stop函数不会关闭通道,以防止从通道读取成功。
为了防止在调用Stop后NewTimer创建的计时器触发,请检查返回值并清空通道。例如,假设程序还没有从t.C接收:
如果!t.Stop(),则需要清空通道:<-t.C
这不能与计时器通道的其他接收并发完成。
我试图理解何时必须手动清空通道。
以下是我的理解,如有错误请指出:
如果Stop返回false,则表示:
  • 计时器已经停止了
    • 在这种情况下,从通道中读取将会阻塞,所以我不应该这样做
  • 计时器已经过期了
    • 由于我有另一个goroutine监听此通道,我能确定它是否已经收到事件吗?
    • 从文档的"这不能与其他接收并发地完成"部分来看,似乎这不是一个选项,那么我该怎么办?

在我的情况下,从计时器获得一个多余的事件并不是什么大问题,这是否可以告诉我在这里该做什么?


1
请查看此链接(https://github.com/golang/go/issues/14383)以及其中的评论(https://github.com/golang/go/issues/14383#issuecomment-220655783)中的讨论。 - kostix
1
你能更详细地描述一下你的原始问题吗?我问这个问题是因为有更多的背景信息可能会使我们提出更多的选项,例如将context.Context与设置了截止时间的监听goroutine传递,并在需要时从外部取消它。 - kostix
1
为什么?为什么你需要在不同的程序中停止一个计时器?为什么不传递一个可以取消的上下文呢?这里有一个select结构和一个专门用于此目的的context.WithCancel... - Elias Van Ootegem
@EliasVanOotegem,我最近开始使用 [tag:go],我还没有听说过 context.WithCancel,谢谢你指出来。后来我想到了一种更符合惯用法的解决问题的方法,不需要在单独的例程中处理计时器。 - Motti
1个回答

9
您可能需要排空通道的原因是goroutine的调度方式。
问题:
假设以下情况:
1. 创建一个定时器 2. 定时器触发,向`t.C`发送一个值 3. 尚未接收/读取该值,但已调用`t.Stop()`
在这种情况下,通道`t.C`上有一个值,并且`t.Stop()`返回false,因为“定时器已经过期”(即当它在`t.C`上发送值时)。
文档中说“这不能与其他接收并发进行”的原因是因为在`if!t.Stop{`和`<-t.C`之间没有排序保证。停止命令可能返回false,进入if主体。然后,另一个goroutine可以被调度并从` t.C`读取if语句体试图排空的值。这将导致数据竞争并导致if语句内部阻塞。(正如您在问题中指出的那样!)
解决方案:
这取决于监听定时器的东西的行为是什么。
如果您只是处于简单的选择状态:
select {
    case result <- doWork():
    case <-t.C
}

如上所述,以下情况可能会发生:

  1. 工作可以从 doWork 中完成,并发送结果,一切正常。
  2. 计时器可能会触发超时,从而中断选择(select)操作。
  3. 调用停止功能,停止计时器,退出选择操作的唯一方法是 doWork() 完成。
  4. 计时器可能会触发超时,另一个例程调用了 t.Stop(),但由于值已被发送,因此太晚了,导致选择操作失败。

只要您接受第四种情况,您就不需要在调用 Stop 后与通道进行交互/排空。

如果您不能接受第四种情况。仍然无法排空t.C通道,因为还有另一个goroutine正在监听它。这可能会在if语句中阻塞。相反,您必须找到另一种布置代码的方式,或确保您在选择操作的goroutine上不再监听该频道。


1
谢谢,幸运的是我对#4没问题,否则我就得做一些实际的工作了 :) - Motti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接