英文:
Golang: using multiple tickers cases in single select blocks entire loop
问题
我有一个需求,需要在一些固定的时间间隔内执行多个操作(这里不相关)。我使用下面提到的代码块实现了这个需求:
func (processor *Processor) process() {
defaultTicker := time.NewTicker(time.Second*2)
updateTicker := time.NewTicker(time.Second*5)
heartbeatTicker := time.NewTicker(time.Second*5)
timeoutTicker := time.NewTicker(30*time.Second)
refreshTicker := time.NewTicker(2*time.Minute)
defer func() {
logger.Info("processor for ", processor.id, " exited")
defaultTicker.Stop()
timeoutTicker.Stop()
updateTicker.Stop()
refreshTicker.Stop()
heartbeatTicker.Stop()
}()
for {
select {
case <-defaultTicker.C:
// spawn some go routines
case <-updateTicker.C:
// do something
case <-timeoutTicker.C:
// do something else
case <-refreshTicker.C:
// log
case <-heartbeatTicker.C:
// push metrics to redis
}
}
}
但是我注意到,偶尔我的 for select
循环会在某个地方卡住,我无法找到卡住的原因和位置。所谓的卡住是指我停止接收刷新计时器的日志。但是它会在一段时间后(5-10分钟)恢复正常工作。
我确保每个计时器内的操作都在非常短的时间内完成(约为0毫秒,通过日志记录进行检查)。
我的问题:
- 在单个
select
中使用多个计时器是一个好的/正常的做法吗(老实说,我在网上没有找到很多使用多个计时器的示例)? - 有人知道任何已知的问题/陷阱,计时器可能会阻塞循环更长的时间吗?
感谢任何帮助。谢谢。
英文:
I have a requirements where I need to do multiple things (irrelevant here) at some regular intervals. I achieved it using the code block mentioned below -
func (processor *Processor) process() {
defaultTicker := time.NewTicker(time.Second*2)
updateTicker := time.NewTicker(time.Second*5)
heartbeatTicker := time.NewTicker(time.Second*5)
timeoutTicker := time.NewTicker(30*time.Second)
refreshTicker := time.NewTicker(2*time.Minute)
defer func() {
logger.Info("processor for ", processor.id, " exited")
defaultTicker.Stop()
timeoutTicker.Stop()
updateTicker.Stop()
refreshTicker.Stop()
heartbeatTicker.Stop()
}()
for {
select {
case <-defaultTicker.C:
// spawn some go routines
case <-updateTicker.C:
// do something
case <-timeoutTicker.C:
// do something else
case <-refreshTicker.C:
// log
case <-heartbeatTicker.C:
// push metrics to redis
}
}
}
But I noticed that every once in a while, my for select loop gets stuck somewhere and I cannot seem to find where or why. By stuck I mean I stop receiving refresh ticker logs. But it starts working again normally in some time (5-10 mins)
I have made sure that all operations within each ticker completes within very little amount of time (~0ms, checked by putting logs).
My questions:
- Is using multiple tickers in single select a good/normal practice (honestly I did not find many examples using multiple tickers online)
- Anyone aware of any known issues/pitfalls where tickers can block the loop for longer duration.
Any help is appreciated. Thanks
答案1
得分: 1
Go语言不提供多个通道的智能排空行为,例如,在一个通道中的旧消息会比其他通道中的最新消息更早地被处理。每次循环进入select
语句时,都会随机选择一个通道。
另请参阅这个答案,并阅读关于GOMAXPROCS=1
的部分。这可能与你的问题有关。问题也可能出现在你的日志包中。也许日志只是延迟了。
总的来说,我认为问题可能出现在你的case
语句中。要么是你有一个阻塞函数,要么是一些功能失效的代码。(注意:由OP确认)
但是回答你的问题:
1. 在单个select
语句中使用多个定时器是一个好的/正常的做法吗?
通常以阻塞的方式从多个通道中随机读取,一次读取一条消息,例如将来自多个通道的输入数据排序到一个切片或映射中,并避免并发数据访问。
通常还会添加一个或多个定时器,例如用于刷新数据、日志记录或报告。通常非定时器的代码路径会执行大部分工作。
在你的情况下,你使用的定时器将运行应该相互阻塞的代码路径,这是一个非常特殊的用例,但在某些情况下可能是必需的。我认为这是不常见但不错的做法。
正如评论者建议的那样,你也可以在单独的goroutine中安排不同的定期任务。
2. 是否有人知道定时器可能导致循环阻塞更长时间的已知问题/陷阱?
定时器本身不会以任何隐藏的方式阻塞循环。最快的定时器将始终确保循环至少以该定时器的速度循环。
请注意,time.NewTicker
的文档中说:
> 定时器将调整时间间隔或丢弃滞后的tick以弥补
> 接收者的速度慢
这只是意味着,在你从单元素定时器通道中消费完最后一个tick之前,不会安排新的tick。
在你的示例中,主要的陷阱是case
语句中的任何代码都会阻塞,从而延迟其他case
的执行。
如果这是你想要的,那就没问题。
如果你有微秒或纳秒级的定时器,可能会出现一些可测量的运行时开销,或者如果你有数百个定时器和case
块,可能会有其他陷阱。但那时你应该从一开始就选择另一种调度模式。
英文:
Go does not provide any smart draining behavior for multiple channels, e.g., that older messages in one channel would get processed earlier than more recent messages in other channels. Anytime the loop enters the select
statement a random channel is chosen.
Also see this answer and read the part about GOMAXPROCS=1
. This could be related to your issue. The issue could also be in your logging package. Maybe the logs are just delayed.
In general, I think the issue must be in your case
statements. Either you have a blocking function or some dysfunctional code. (Note: confirmed by the OP)
But to answer your questions:
1. Is using multiple tickers in single select a good/normal practice?
It is common to read from multiple channels randomly in a blocking way, one message at a time, e.g., to sort incoming data from multiple channels into a slice or map and avoid concurrent data access.
It is also common to add one or more tickers, e.g., to flush data and for logging or reporting. Usually the non-ticker code paths will do most of the work.
In your case, you use tickers that will run code paths that should block each other, which is a very specific use case but may be required in some scenarios. This uncommon, but not bad practice I think.
As the commenters suggested, you could also schedule different recurring tasks in separate goroutines.
2. Is anyone aware of any known issues/pitfalls where tickers can block the loop for longer duration?
The tickers themselves will not block the loop in any hidden way. The fastest ticker will always ensure the loop is looping at the speed of this ticker at least.
Note that the docs of time.NewTicker
say:
> The ticker will adjust the time interval or drop ticks to make up for
> slow receivers
This just means, internally no new ticks are scheduled until you have consumed the last one from the single-element ticker channel.
In your example, the main pitfall is that any code in the case
statements will block and thus delay the other cases.
If this is intended, everything is fine.
There may be other pitfalls if you have Microsecond or Nanosecond tickers where you may see some measurable runtime overhead or if you have hundreds of tickers and case
blocks. But then you should have chose another scheduling pattern from the beginning.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论