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

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

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)
}

它打印出了"Done",就像我预期的那样。

而这个函数却没有:

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)
}

唯一的区别是我在协程内部声明了计时器(timer),而不是在外部声明。mockClockTimer()方法具有指针接收器并返回一个指针。我无法解释为什么第一个代码可以工作而第二个代码不行。

英文:

This code works as I expect it

import (
	&quot;fmt&quot;
	&quot;time&quot;

	&quot;github.com/benbjohnson/clock&quot;
)

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

It prints "Done" as I expect.
Whereas this function does not

import (
	&quot;fmt&quot;
	&quot;time&quot;

	&quot;github.com/benbjohnson/clock&quot;
)

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

The only difference here is I'm declaring the timer outside the goroutine vs. inside it. The mockClock Timer() method has a pointer receiver and returns a pointer. I can't explain why the first one works and the second doesn't.

答案1

得分: 1

benbjohnson/clock提供了模拟时间功能。特别是他们的文档中指出:

定时器和Ticker也由这个模拟时钟控制。只有在时钟向前移动时,它们才会执行。

因此,当您调用mockClock.Add时,它将顺序执行定时器/ Ticker。该库还会人为地添加顺序的1毫秒睡眠,以模拟对其他goroutine的让步。

当定时器/ Ticker在goroutine之外声明时,即在调用mockClock.Add之前,当调用mockClock.Add时,模拟时间确实有要执行的内容。库内部的睡眠足够让子goroutine在Ticker上接收并打印“done”,然后程序退出。

当Ticker在goroutine内部声明时,当调用mockClock.Add时,模拟时间没有要执行的Ticker,Add实际上什么也不做。内部的睡眠确实给子goroutine运行的机会,但是现在在Ticker上接收只会阻塞;然后主程序恢复并退出。

您还可以查看存储库的README中的Ticker示例:

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

// 启动一个定时器,每1个模拟秒递增一次。
go func() {
    ticker := mock.Ticker(1 * time.Second)
    for {
        <-ticker.C
        count++
    }
}()
runtime.Gosched()

// 将时钟向前移动10秒。
mock.Add(10 * time.Second)

// 这将打印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(),它将像存储库的示例一样正常工作。Playground: https://go.dev/play/p/ZitEdtx9GdL

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

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

英文:

The package benbjohnson/clock provides mock time facilities. In particular their documentation states:

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

So when you call mockClock.Add, it will sequentially execute the timers/tickers. The library also adds sequential 1 millisecond sleeps to artificially yield to other goroutines.

When the timer/ticker is declared outside the goroutine, i.e. before calling mockClock.Add, by the time mockClock.Add gets called the mock time does have something to execute. The library's internal sleeps are enough for the child goroutine to receive on the ticker and print "done", before the program exits.

When the ticker is declared inside the goroutine, by the time mockClock.Add gets called, the mock time has no tickers to execute and Add essentially does nothing. The internal sleeps do give a chance to the child goroutine to run, but receiving on the ticker now just blocks; main then resumes and exits.

You can also have a look at the ticker example that you can see in the repository's README:

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

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

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

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

This uses runtime.Gosched() to yield to the child goroutine before calling mock.Add. The sequence of this program is basically:

  • clock.NewMock()
  • count := 0
  • spawn child goroutine
  • runtime.Gosched(), yielding to the child goroutine
  • ticker := mock.Ticker(1 * time.Second)
  • block on &lt;-ticker.C (the mock clock hasn't moved forward yet)
  • resume main
  • mock.Add, which moves the clock forward and yields to the child goroutine again
  • for loop with &lt;-ticker.C
  • print 10
  • exit

By the same logic, if you add a runtime.Gosched() to your second snippet, it will work as expected, just like the repository's example. Playground: https://go.dev/play/p/ZitEdtx9GdL

However, do not rely on runtime.Gosched() in production code, possibly not even in test code, unless you're very sure about what you are doing.

<hr>

Finally, please remember that time.Sleep(1) sleeps for one nanosecond.

huangapple
  • 本文由 发表于 2022年4月12日 12:17:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/71837053.html
匿名

发表评论

匿名网友

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

确定