为什么 benbjohnson/clock 模拟计时器在 goroutine 中声明时不执行?

Why does benbjohnson/clock mock timer not execute when declared inside a goroutine?

此代码如我所料工作

import (
    "fmt"
    "time"

    "github.com/benbjohnson/clock"
)

func main() {
    mockClock := clock.NewMock()
    timer := mockClock.Timer(time.Duration(2) * time.Second)
    go func() {
        <-timer.C
        fmt.Println("Done")
    }()
    mockClock.Add(time.Duration(10) * time.Second)
    time.Sleep(1)
}

如我所料打印“完成”。 而这个函数没有

import (
    "fmt"
    "time"

    "github.com/benbjohnson/clock"
)

func main() {
    mockClock := clock.NewMock()
    go func() {
        timer := mockClock.Timer(time.Duration(2) * time.Second)
        <-timer.C
        fmt.Println("Done")
    }()
    mockClock.Add(time.Duration(10) * time.Second)
    time.Sleep(1)
}

这里唯一的区别是我是在 goroutine 外部声明计时器还是在 goroutine 内部声明计时器。 mockClock Timer() 方法有一个指针接收器和 returns 一个指针。我无法解释为什么第一个有效而第二个无效。

benbjohnson/clock 提供 mock 时间工具。特别是他们的文档状态:

Timers and Tickers are also controlled by this same mock clock. They will only execute when the clock is moved forward

因此当您调用 mockClock.Add 时,它将顺序 执行 timers/tickers。该库还添加了连续的 1 毫秒睡眠,以人为地屈服于其他 goroutines。

当 timer/ticker 在 goroutine 外部声明时,即在调用 mockClock.Add 之前,到 mockClock.Add 被调用时,模拟时间确实有一些东西要执行。在程序退出之前,库的内部睡眠足以让子 goroutine 在自动收报机上接收并打印“完成”。

当代码在 goroutine 中声明时,到 mockClock.Add 被调用时,模拟时间没有代码可执行,Add 基本上什么都不做。内部睡眠确实给了子 goroutine 到 运行 的机会,但是现在在自动收报机上接收只是阻塞; main 然后恢复并退出。

您还可以查看存储库自述文件中的代码示例:

mock := clock.NewMock()
count := 0

// Kick off a timer to increment every 1 mock second.
go func() {
    ticker := mock.Ticker(1 * time.Second)
    for {
        <-ticker.C
        count++
    }
}()
runtime.Gosched()

// Move the clock forward 10 seconds.
mock.Add(10 * time.Second)

// This prints 10.
fmt.Println(count)

这使用 runtime.Gosched() 在 调用 mock.Add 之前屈服于子 goroutine 。这个程序的顺序基本上是:

  • clock.NewMock()
  • count := 0
  • 生成子 goroutine
  • runtime.Gosched(),让步给子 goroutine
  • ticker := mock.Ticker(1 * time.Second)
  • 阻塞 <-ticker.C(模拟时钟尚未向前移动)
  • 恢复主要
  • mock.Add,将时钟向前移动并再次让步给子 goroutine
  • for 循环 <-ticker.C
  • 打印 10
  • 退出

按照相同的逻辑,如果您将 runtime.Gosched() 添加到第二个代码段,它将按预期工作,就像存储库的示例一样。游乐场:https://go.dev/play/p/ZitEdtx9GdL

但是,请勿在生产代码中依赖 runtime.Gosched(),甚至在测试代码中也不要依赖,除非您非常确定自己在做什么。


最后,请记住 time.Sleep(1) 睡眠一 nano 秒。