在Golang中,可以使用多个Ticker在单个select语句中来实现整个循环。

huangapple go评论76阅读模式
英文:

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毫秒,通过日志记录进行检查)。

我的问题:

  1. 在单个 select 中使用多个计时器是一个好的/正常的做法吗(老实说,我在网上没有找到很多使用多个计时器的示例)?
  2. 有人知道任何已知的问题/陷阱,计时器可能会阻塞循环更长的时间吗?

感谢任何帮助。谢谢。

英文:

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(&quot;processor for &quot;, processor.id, &quot; exited&quot;)
		defaultTicker.Stop()
		timeoutTicker.Stop()
		updateTicker.Stop()
		refreshTicker.Stop()
		heartbeatTicker.Stop()
	}()
	for {
		select {
		case &lt;-defaultTicker.C:
			// spawn some go routines
		case &lt;-updateTicker.C:
			// do something
		case &lt;-timeoutTicker.C:
			// do something else
		case &lt;-refreshTicker.C:
			// log
		case &lt;-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:

  1. Is using multiple tickers in single select a good/normal practice (honestly I did not find many examples using multiple tickers online)
  2. 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.

huangapple
  • 本文由 发表于 2021年9月16日 03:34:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/69198865.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定