Golang time.Ticker 在阻塞一段时间后触发两次。

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

Golang time.Ticker triggers twice after blocking a while

问题

我正在使用time.Ticker与一些阻塞代码,并发现了一种奇怪的行为:

func main() {
    ticker := time.NewTicker(1 * time.Second)

    i := 0
    for {
        select {
        case <-ticker.C:
            log.Println(i)
            if i == 0 {
                time.Sleep(5 * time.Second) // 模拟阻塞
            }
            i++
        }
    }
}

一个示例输出:

2022/02/20 10:49:45 0
2022/02/20 10:49:50 1 <- 同时显示
2022/02/20 10:49:50 2 <- 同时显示
2022/02/20 10:49:51 3
2022/02/20 10:49:52 4
2022/02/20 10:49:53 5
...

为什么"1"和"2"总是同时显示?

英文:

I'm using time.Ticker with some blocking code, found a strange behavior:

func main() {
	ticker := time.NewTicker(1 * time.Second)

	i := 0
	for {
		select {
		case &lt;-ticker.C:
			log.Println(i)
			if i == 0 {
				time.Sleep(5 * time.Second) // simulate the blocking
			}
			i++
		}
	}
}

A sample output:

2022/02/20 10:49:45 0
2022/02/20 10:49:50 1 &lt;- shows at same time
2022/02/20 10:49:50 2 &lt;- shows at same time
2022/02/20 10:49:51 3
2022/02/20 10:49:52 4
2022/02/20 10:49:53 5
...

The "1" and "2" always shown at same time, why is that?

答案1

得分: 2

这里是问题中跟踪的时间线。在同一时间列出的事件会稍微晚于按顺序列出的其他事件。

1秒 - ticker 发送值
1秒 - 主 goroutine 接收第一个值
1秒 - 主 goroutine 开始休眠5秒
2秒 - ticker 发送值(通道容量为1)。
3秒 - ticker 无法发送值,因为通道已满
4秒 - ticker 无法发送值,因为通道已满
5秒 - ticker 无法发送值,因为通道已满
6秒 - 主 goroutine 退出休眠并接收缓冲值
6秒 - ticker 发送值
6秒 - 主 goroutine 接收缓冲值

ticker 和主 goroutine 在6秒标记处进行竞争,因为休眠时间是 ticker 周期的倍数。在你的跟踪中,主 goroutine 赢得了比赛。在我的机器上,ticker 赢得了比赛,我得到了你期望的时间。在我的机器上看起来是这样的。

...
5秒 - ticker 无法发送值,因为通道已满
6秒 - ticker 无法发送值,因为通道已满
6秒 - 主 goroutine 接收缓冲值
7秒 - ticker 发送值
7秒 - 主 goroutine 接收值

ticker 在 playground 上赢得了到6秒标记的比赛。在这里运行。当休眠时间被减少1微秒时,主 goroutine 赢得了比赛。在这里运行

当休眠持续时间不是 ticker 周期的倍数时,这一点很容易看出。以下是当休眠持续时间为2.5秒时发生的情况:

1秒 - ticker 发送值
1秒 - 主 goroutine 接收第一个值
1秒 - 主 goroutine 开始休眠2.5秒
2秒 - ticker 发送值
3秒 - ticker 无法发送值,因为通道已满
3.5秒 - 主 goroutine 退出休眠并接收缓冲值
4秒 - ticker 发送值
4秒 - 主 goroutine 接收值

在 playground 上运行最后一个示例

英文:

The ticker creates a buffered channel with a capacity of one. The ticker drops time values when the channel is full.

Here's the timeline for the trace in the question. Events listed for the same time happen a tiny bit after each other in the order listed.

1s - ticker sends value
1s - main goroutine receives first value
1s - main goroutine starts sleep for 5s
2s - ticker sends value (channel has capacity 1).
3s - ticker fails to send value because channel is full
4s - ticker fails to send value because channel is full
5s - ticker fails to send value because channel is full
6s - main goroutine exits sleep and receives buffered value
6s - ticker sends value
6s - main goroutine receives buffered value

The ticker and main goroutine race to the 6s mark because the sleep is a multiple of the ticker period. In your trace, the main goroutine wins the race. On my machine, the ticker wins the race and I get the timing that you expect. Here's what it looks like on my machine.

...
5s - ticker fails to send value because channel is full
6s - ticker fails to send value because channel is full
6s - main goroutine receives buffered value
7s - ticker sends value
7s - main goroutine receives value

The ticker wins the race to the 6s mark on the playground. Run it here.
The main goroutine wins the race when the sleep time is nudged down by a 1µs. Run it here.

This is easy to see when the sleep duration is not a multiple of the ticker period. Here's what happens when the sleep duration is 2.5 seconds:

1s - ticker sends value
1s - main goroutine receives first value
1s - main goroutine starts sleep for 2.5s
2s - ticker sends value
3s - ticker fails to send value because channel is full
3.5s - main goroutine exits sleep and receives buffered value
4s - ticker sends value
4s - main goroutine receives value

Run the last example on the playground.

答案2

得分: 1

time.Ticker中,有一个chan Time。在NewTicker(d Duration) *Ticker函数中,它创建了一个带有容量为1的缓冲通道作为tickers Time通道。在函数中,提到了以下内容。

// 给通道一个1个元素的时间缓冲区。
// 如果客户端在读取时落后,我们会丢弃ticks,直到客户端赶上。

在你的情况下,时间每秒钟进行一次打点并写入通道。在睡眠时,ticker向通道写入一次时间,然后在循环继续之后立即被select case接收并打印出来。在接下来的4秒钟里,通道被阻塞并且ticks被丢弃。循环立即继续后,通道解除阻塞并接收更多的ticks到通道中。

打印通过通道传递的时间,你可以很容易地理解这种行为。更新后的代码如下。

func main() {
    ticker := time.NewTicker(1 * time.Second)

    i := 0
    for {
        select {
        case t := <-ticker.C:
            log.Println(i, t)
            if i == 0 {
                time.Sleep(5 * time.Second) // 模拟阻塞
            }
            i++
        }
    }
}

输出:

2022/02/20 08:47:58 0 2022-02-20 08:47:58.773375 +0530 +0530 m=+1.004241004
2022/02/20 08:48:03 1 2022-02-20 08:47:59.772787 +0530 +0530 m=+2.003666993
2022/02/20 08:48:03 2 2022-02-20 08:48:03.774272 +0530 +0530 m=+6.005207433 // 打印的时间相同,但通道接收的时间相差4秒。
2022/02/20 08:48:04 3 2022-02-20 08:48:04.774203 +0530 +0530 m=+7.005151715

Playground中运行。

英文:

In time.Ticker, there is a chan Time. In the NewTicker(d Duration) *Ticker function, It creates tickers Time channel as a buffered channel with capacity 1. In the function, it is mentioned as below.

> // Give the channel a 1-element time buffer.
> // If the client falls behind while reading, we drop ticks
> // on the floor until the client catches up.

In your case, time ticks in every second and write to the channel. While sleeping, the ticker writes one time to the channel, and after the loop continues it is immediately received by select case and print. For the other 4 seconds, the channel is blocking and ticks are dropped. Immediately loop continues it is unblocked and accept more ticks to the channel.

Print the time comes through the channel and you can easily understand the behavior. Updated code as below.

func main() {
	ticker := time.NewTicker(1 * time.Second)

	i := 0
	for {
		select {
		case t := &lt;-ticker.C:
			log.Println(i, t)
			if i == 0 {
				time.Sleep(5 * time.Second) // simulate the blocking
			}
			i++
		}
	}
}

Output:

2022/02/20 08:47:58 0 2022-02-20 08:47:58.773375 +0530 +0530 m=+1.004241004
2022/02/20 08:48:03 1 2022-02-20 08:47:59.772787 +0530 +0530 m=+2.003666993
2022/02/20 08:48:03 2 2022-02-20 08:48:03.774272 +0530 +0530 m=+6.005207433 // printing time is same, but channel&#39;s received time has 4 sec difference.
2022/02/20 08:48:04 3 2022-02-20 08:48:04.774203 +0530 +0530 m=+7.005151715

Run in Playground.

huangapple
  • 本文由 发表于 2022年2月20日 11:06:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/71191067.html
匿名

发表评论

匿名网友

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

确定