重置代码

Resetting a ticker

考虑一个使用 timer/ticker 资源初始化的服务器,该资源将在每个 tt 在我的示例中为 20 毫秒)滴答。每次服务器在网络上监听某些内容(例如来自对等方的周期性信号)时,它都必须重置计时器。另一方面,如果计时器在没有重置的情况下到期(例如,它的所有对等点都死了),它会触发一些事件(在我的示例中,我只是打印从程序开始的时间)。

我在使用 time.Ticker 实现此行为时遇到问题。重置计时器似乎有效(它不会在前 50 毫秒触发),但之后自动收报机不活动(不会每 20 毫秒打勾一次)。

package main

import (
    "fmt"
    "time"
)

var wallclock time.Time

type server struct {
    timeout *time.Ticker
    stop    chan bool
}

func (srv *server) start() {
    for {
        select {
        case <-srv.timeout.C:
            {
                elapsed := time.Since(wallclock)
                fmt.Println("timed out after ", elapsed, " elapsed from start ")
            }
        case <-srv.stop:
            {
                return
            }
        }
    }
}

func main() {
    wallclock = time.Now()
    //make the server with a timer that will fire every 20ms
    srv := server{
        timeout: time.NewTicker(20 * time.Millisecond),
        //channel to indicate the server to stop listening
        stop:    make(chan bool),
    }
    //start listening on a different thread 
    go srv.start()
    for i := 0; i < 5; i++ {
        //reset it every 10ms
        time.Sleep(10 * time.Millisecond)
        srv.timeout.Stop()
        //as the reset frequency is higher, 
        //I'm not expecting this to fire within 
        //the first 50ms (5*10ms)
        srv.timeout = time.NewTicker(20 * time.Millisecond)
    }
    //sleep for 110ms
    //I'm expecting the timer to fire at least 5 times here
    time.Sleep(110 * time.Millisecond)
    //stop listening
    srv.stop <- true
    fmt.Println("Hi from tckr!")
}

我期待看到类似

的内容
timed out after ~70ms elapsed from start
timed out after ~90ms elapsed from start
timed out after ~110ms elapsed from start
timed out after ~130ms elapsed from start
timed out after ~150ms elapsed from start
Hi from tckr!

五次,因为我让主线程休眠了 110 毫秒,而 20 毫秒计时器可以在此间隔内触发五次。

但我刚看到Hi from tckr!srv.timeout = time.NewTicker(20 * time.Millisecond) 是重置 Ticker 的正确方法吗?

如果我不在 for 循环 (srv.timeout.Stop()) 中停止自动收报机,则自动收报机似乎会继续运行。这是评论 srv.timeout.Stop() 后的示例输出。

timed out after  20.6872ms  elapsed from start
timed out after  41.4278ms  elapsed from start
timed out after  61.8747ms  elapsed from start
timed out after  72.7793ms  elapsed from start
timed out after  94.1448ms  elapsed from start
timed out after  112.5283ms  elapsed from start
timed out after  134.0131ms  elapsed from start
timed out after  152.5846ms  elapsed from start
Hi from tckr!

我不希望自动收报机在前 50 毫秒内触发(即,我不希望看到前两行分别为 20.6872 毫秒和 41.4278 毫秒)。

记住,for select statements:

For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the "select" statement. The result is a set of channels to receive from or send to, and the corresponding values to send.

这意味着一旦 start 进入其 select 语句,它就会评估 srv.timeout.C 并保持通道; srv.timeout 进入 select 后对 srv.timeout 的任何更改都不会影响 select,它仍将等待从之前的频道接收。

您可以通过添加另一个通道(在此处名为 ping 的示例中)来解决此问题,这样您就可以向 start 发出通道正在改变的信号(或者可能移动整个重置逻辑进入 start):

type server struct {
    timeout *time.Ticker
    stop    chan bool
    ping    chan struct{}
}

func (srv *server) start() {
    for {
        select {
        case <-srv.timeout.C:
            elapsed := time.Since(wallclock)
            fmt.Println("timed out after ", elapsed, " elapsed from start ")
        case <-srv.ping:
            // do nothing & let the loop iterate
            // OR
            srv.timeout.Stop()
            srv.timeout = time.NewTicker(20 * time.Millisecond)
        case <-srv.stop:
            return
        }
    }
}

// in main()
go srv.start()
for i := 0; i < 5; i++ {
    //reset it every 10ms
    time.Sleep(10 * time.Millisecond)
    srv.ping <- struct{}{}

    // possibly shift the below logic to start()'s ping handler case
    srv.timeout.Stop()
    srv.timeout = time.NewTicker(20 * time.Millisecond)
}