如何使time.Tick立即开始计时?

43

我有一个循环,一直迭代直到工作开始运行:

ticker := time.NewTicker(time.Second * 2)
defer ticker.Stop()

started := time.Now()
for now := range ticker.C {
    job, err := client.Job(jobID)
    switch err.(type) {
    case DoesNotExistError:
        continue
    case InternalError:
        return err
    }

    if job.State == "running" {
        break
    }

    if now.Sub(started) > time.Minute*2 {
        return fmt.Errorf("timed out waiting for job")
    }
}

在生产环境中非常好用。唯一的问题是它会使我的测试变慢。所有测试在完成之前都要等待至少2秒钟。是否有办法让time.Tick立即开始计时?


哎呀,感谢提醒。 - Xavi
在测试中将股票和其他时间间隔设置为较小的值。 - Charlie Tumahai
这些“作业”实际上是在远程服务上运行的。虽然可以让该程序在本地运行服务器并修改远程服务以接受订阅,但我认为这种解决方案过于笨重。 - Xavi
@JimB 对不起,我很无知,请问你所说的job.State竞态条件是什么?假设job是一个共享对象吗? - Xavi
抱歉,我当时在手机上看,误读了代码,以为 job 是来自于 for 循环外部。 - JimB
2
@Xavi:将你的代码封装在一个函数中,使用变量来控制ticker延迟和超时值,在测试时将这些变量设置为较小的值。 - LeGEC
8个回答

70

不幸的是,似乎Go开发人员在可预见的未来不会添加这样的功能,所以我们必须应对...

使用滴答器的两种常见方式:

for循环

假设有以下代码:

ticker := time.NewTicker(period)
defer ticker.Stop()
for <- ticker.C {
    ...
}

使用:

ticker := time.NewTicker(period)
defer ticker.Stop()
for ; true; <- ticker.C {
    ...
}

for-select 循环

假设有如下代码:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()

loop:
for {
    select {
        case <- ticker.C: 
            f()
        case <- interrupt:
            break loop
    }
}

使用:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()

loop:
for {
    f()

    select {
        case <- ticker.C: 
            continue
        case <- interrupt:
            break loop
    }
}

为什么不直接使用time.Tick()

虽然Tick对于不需要关闭Ticker的客户端很有用,但请注意,如果没有关闭它的方法,底层的Ticker将无法被垃圾收集器回收;它会"泄漏"。

https://golang.org/pkg/time/#Tick


2
<- time.Tick(period) 每次迭代都会创建一个 Ticker 吗?根据文档,由 Tick 创建的 Ticker 无法进行垃圾回收。这让我感到恐惧。 - youfu
1
@youfu 你说得完全正确!我刚刚编辑了我的回答。 - Bora M. Alper

26

7
补充一下,如果你不想失去通过 chan 获得的价值,你可以使用 for t := time.Now(); true; t = <-ticker.C { - Devon Peticolas
这种解决方案在需要在for循环块内调用continue的情况下也更加优雅。 - natenho

6

Ticker 的实际实现内部相当复杂。但你可以用 goroutine 包装它:

func NewTicker(delay, repeat time.Duration) *time.Ticker {
    ticker := time.NewTicker(repeat)
    oc := ticker.C
    nc := make(chan time.Time, 1)
    go func() {
        nc <- time.Now()
        for tm := range oc {
            nc <- tm
        }
    }()
    ticker.C = nc
    return ticker
}

你不能立即将消息发送到股票频道吗? - LenW
2
这是一个只接收的通道:type Ticker struct { C <-chan Time // 用于传递tick的通道。 // 包含过滤或未公开的字段 } - Caleb
为什么这里要使用 delay 参数? - coanor
“延迟”未被使用。此计时器不能保证立即触发,因为go func(){}部分是被调度的但不一定被执行。除了这点挑刺以外,我认为这应该是被接受的答案。 - simon

6

如果您想立即检查作业,请勿在for循环中使用ticker作为条件。例如:

ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()

started := time.Now()
for {
    job, err := client.Job(jobID)
    if err == InternalError {
        return err
    }

    if job.State == "running" {
        break
    }

    now := <-ticker.C
    if now.Sub(started) > 2*time.Minute {
        return fmt.Errorf("timed out waiting for job")
    }
}
如果您仍需要检查DoesNotExistError,请确保在股票信息后进行,以避免繁忙等待。

在我看来,这是正确的解决方案 - 也存在于stdlib中,请参见https://github.com/golang/go/blob/master/src/net/http/server.go#L2648 - jwilner

2
我设计了类似这样的东西。
func main() {
    t := time.Now()
    callme := func() {
        // do somethign more
        fmt.Println("callme", time.Since(t))
    }
    ticker := time.NewTicker(10 * time.Second)
    first := make(chan bool, 1)
    first <- true
    for {
        select {
        case <-ticker.C:
            callme()
        case <-first:
            callme()
        }
        t = time.Now()
    }
    close(first)
}

在循环之前调用callme()不仅更容易,而且更简洁。使它变得复杂有什么好处呢? - Alexandru Eftimie

1
我认为这可能是一个有趣的替代方案,用于 for-select 循环,特别是当 case 的内容不是一个简单的函数时:

假设:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()

loop:
for {
    select {
        case <- ticker.C: 
            f()
        case <- interrupt:
            break loop
    }
}

使用:

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)

ticker := time.NewTicker(period)
defer ticker.Stop()
firstTick := false

// create a wrapper of the ticker that ticks the first time immediately
tickerChan := func() <-chan time.Time {
  if !firstTick {
    firstTick = true
    c := make(chan time.Time, 1)
    c <- time.Now()
    return c
  }

  return ticker.C
}

loop:
for {
    select {
        case <- tickerChan(): 
            f()
        case <- interrupt:
            break loop
    }
}

1

你也可以在循环结束时排空通道:

t := time.NewTicker(period)
defer t.Stop()

for {
    ...
    
    <-t.C
}

1

使用Timer而不是Ticker如何? Timer可以从零持续时间开始,然后重置为所需的持续时间值:

timer := time.NewTimer(0)
defer timer.Stop()

for {
    select {
        case <-timer.C:
            timer.Reset(interval)
            job()
        case <-ctx.Done():
            break
    }
}

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