On the correct posture used by Golang Timer Reset method

Source: Internet
Author: User
Tags gopher
This is a creation in Article, where the information may have evolved or changed.

In 2016, the go language ranked sharply in the Tiobe programming language rankings (December 2016 Tiobe list: Go ranked 16th, rating value: 1.939%). At the same time, we can also personally feel the go language in the world, the development of its land in China is particularly violent ^0^:for gopher job changes, online information about Go is also a lot of "voluminous" trend. As a professional gopher^0^, to contribute to this ecological, it is necessary to think more, more summary, the key is to do "encounter problems, it is necessary to speak out, give your opinion." Each article has its own cut-in angle and focus, so gopher do not have to worry too much about the "repetition" of the data.

This time, I'm talking about the problem with the Reset method of the timer in the Go standard library.

I. Some notes on the principle of the timer

In network programming, from the user's point of view, Golang is a "blocking" network programming paradigm, while supporting this "blocking" paradigm is built into the go-compiled executable file runtime. The runtime uses the network IO multiplexing mechanism to realize the reasonable dispatching of multiple network communication goroutine. The execution function in Goroutine is equivalent to the callback function you pass to the epoll mechanism in traditional C programming. Golang has eliminated the mental burden of "reverse thinking" in this area, simplifying the complexity of network programming.

However, long-time "blocking" obviously does not satisfy most business scenarios, so a certain time-out mechanism is also required. For example: At the socket level, we explicitly set net. Dialer timeout or use Setreaddeadline, setwritedeadline, and Setdeadline; In the application layer protocol, such as http,client by setting the timeout parameter, The server uses Timeouthandler to limit the time limit of the operation. Some of these timeout mechanisms are implemented through the runtime's network multiplexing timeout mechanism, while others are implemented by the timer.

The timer in the standard library allows users to define their own time-out logic, especially if the select handles multiple channel timeouts, single-channel read-write timeouts, and so on.

1, the creation of the timer

A timer is a one-time trigger event, which is different from ticker, which is a constant trigger time event at a certain time interval. The common usage scenarios for a timer are as follows:

场景1:t := time.AfterFunc(d, f)场景2:select {    case m := <-c:       handle(m)    case <-time.After(5 * time.Minute):       fmt.Println("timed out")}或:t := time.NewTimer(5 * time.Minute)select {    case m := <-c:       handle(m)    case <-t.C:       fmt.Println("timed out")}

From these two scenarios, we can see the timer three creation posture:

t:= time.NewTimer(d)t:= time.AfterFunc(d, f)c:= time.After(d)

Although the posture is different, the principle behind it is interlinked.

There are three elements of a timer:

* 定时时间:也就是那个d* 触发动作:也就是那个f* 时间channel: 也就是t.C

For Afterfunc this way of creation, the timer is executing function f after timeout (timer expire), in which case: Time channel is useless.

//$GOROOT/src/time/sleep.gofunc AfterFunc(d Duration, f func()) *Timer {    t := &Timer{        r: runtimeTimer{            when: when(d),            f:    goFunc,            arg:  f,        },    }    startTimer(&t.r)    return t}func goFunc(arg interface{}, seq uintptr) {    go arg.(func())()}

Note: From the Afterfunc source can be seen, the outside of the F parameter is not directly assigned to the internal F, but as the wrapper Function:gofunc Arg passed in. Gofunc, in turn, initiates a new goroutine to execute the externally imported F. This is because the execution of the timer expire corresponding event handler is the only Timer events maintenance Goroutine:timerproc within go runtime. In order not to execute the block Timerproc, a new goroutine must be started.

//$GOROOT/src/runtime/time.gofunc timerproc() {    timers.gp = getg()    for {        lock(&timers.lock)        ... ...            f := t.f            arg := t.arg            seq := t.seq            unlock(&timers.lock)            if raceenabled {                raceacquire(unsafe.Pointer(t))            }            f(arg, seq)            lock(&timers.lock)        }        ... ...        unlock(&timers.lock)   }}

For both Newtimer and after, the timer executes a function built into the standard library after a timeout (timer expire): Sendtime. Sendtime send the current event to the time channel of the timer, does this action not block the execution of the Timerproc? The answer is definitely not, the reason is in the following code:

//$GOROOT/src/time/sleep.gofunc NewTimer(d Duration) *Timer {    c := make(chan Time, 1)    t := &Timer{        C: c,        ... ...    }    ... ...    return t}func sendTime(c interface{}, seq uintptr) {    // Non-blocking send of time on c.    // Used in NewTimer, it cannot block anyway (buffer).    // Used in NewTicker, dropping sends on the floor is    // the desired behavior when the reader gets behind,    // because the sends are periodic.    select {    case c.(chan Time) <- Now():    default:    }}

We see that Newtimer created a buffered channel,size = 1. Normally, when the timer expire,t.c whether or not there is goroutine in the Read,sendtime can non-block the current time to send to C; At the same time, we see that sendtime also added double insurance: A select to determine the C Buffer is full, once full, exit directly, still not block, this situation may be encountered when reuse active timer.

2, the timer's resource release

Many go beginners use timers to worry that a timer's creation consumes system resources, such as:

One would think that when a timer is created, the runtime creates a separate goroutine to clock and sends the current time to the channel after expire.
Others think: After a timer is created, runtime will request an OS-level timer resource to complete the timekeeping work.

This is not the case. Just recently Gopheracademy blog published a "How does they do it:timers in Go", through the analysis of the source of the timer, told the principle of the timer, we can see.

Go runtime actually just launches a separate goroutine, runs the Timerproc function, maintains a "minimum heap", periodically wake up, reads the timer on the top of the heap, executes the timer corresponding to the F function, and removes the timer element. Creating a timer is actually adding a element,stop to the smallest heap. A timer is the deletion of the corresponding element from the heap.

At the same time, judging from the code in the two common usage scenarios above, we don't explicitly release anything. As we can see from the previous section, the resources that the timer may occupy after it is created also include:

    • 0 or one channel
    • 0 or a Goroutine

These resources will be collected by GC after the timer is used.

In summary, as the user of the timer, what we have to do is to minimize the pressure on the minimum heap management Goroutine and GC when using the timer, namely: Call the timer's Stop method in time to remove the timer element from the minimum heap (if the timer No expire) and reuse active timer.

BTW, there is also an article about the accuracy of Go timer, you can read it.

Second, what is the problem of reset exactly?

Cushion so much, mainly to illustrate the use of reset problems. What's the problem? Let's take a look at the following example. These examples are primarily intended to illustrate the reset problem, and in reality it is very likely that we do not write code logic like this. Current environment: Go version go1.7 darwin/amd64.

1, example1

Our first example is as follows:

Example1.gofunc Main () {c: = make (chan bool) go func () {for i: = 0; i < 5; i++ {time. Sleep (time. Second * 1) C <-false} time. Sleep (time. Second * 1) C <-true} () go func () {for {//-try to read from channel, block at the most 5            S.//If timeout, print time event and go on loop.            If read a message which is isn't the type we want (we want true, not false),//Retry to read. Timer: = time. Newtimer (time. Second * 5) Defer timer. Stop () Select {case b: = <-c:if b = = False {fmt. Println (time. Now (), ": recv false. Continue ") Continue}//we want true, not false fmt. Println (time. Now (), ": recv true. Return ") return case <-timer. C:fmt. Println (time. Now (), ": Timer expired") CONtinue}}} ()//to avoid that all goroutine blocks. var s string fmt. Scanln (&s)}

The logic of Example1.go is roughly a consumer goroutine trying to read true from a channel, if it reads false or the timer expire, then continue with the try to read from the channel. Here we create a timer for Each loop, and stop the timer after the go routine ends. Another producer Goroutine is responsible for producing messages and sending them to the channel. The behavior that actually occurs in consumer depends on the sending behavior of the producer Goroutine.

The results of example1.go execution are as follows:

$go run example1.go2016-12-21 14:52:18.657711862 +0800 CST :recv false. continue2016-12-21 14:52:19.659328152 +0800 CST :recv false. continue2016-12-21 14:52:20.661031612 +0800 CST :recv false. continue2016-12-21 14:52:21.662696502 +0800 CST :recv false. continue2016-12-21 14:52:22.663531677 +0800 CST :recv false. continue2016-12-21 14:52:23.665210387 +0800 CST :recv true. return

Output as expected. But in this process, we have created 6 new timer.

2, Example2

If we do not want to repeat the creation of so many timer instances, but rather reuse the existing timer instance, then we need to use the timer reset method, see below Example2.go, consider the space, here only consumer routine code is listed, Others remain the same:

Example2.go.//Consumer routine go func () {//try to read from channel, block at most 5s.        If timeout, print time event and go on loop.        If read a message which is isn't the type we want (we want true, not false),//Retry to read. Timer: = time. Newtimer (time.            Second * 5) for {//timer are active, not fired, stop always returns TRUE, no problems occurs. If!timer. Stop () {<-timer. C} timer. Reset (time. Second * 5) Select {case b: = <-c:if b = = False {fmt. Println (time. Now (), ": recv false. Continue ") Continue}//we want true, not false fmt. Println (time. Now (), ": recv true. Return ") return case <-timer. C:fmt. Println (time. Now (), ": Timer expired") Continue}}} () ...

Follow the recommendations for reset using go 1.7 doc:

To reuse an active timer, always call its Stop method first and—if it had expired—drain the value from its channel. For example:if !t.Stop() {        <-t.C}t.Reset(d)

We have transformed the example1 to form the example2 code. Because the producer behavior is not changed, the actual example2 execution, each cycle of the timer before reset is not expire, there is no fire a time to channel, so the timer. The call to stop returns True, which is the successful removal of the timer from the minimum heap. The results of the EXAMPLE2 implementation are as follows:

$go run example2.go2016-12-21 15:10:54.257733597 +0800 CST :recv false. continue2016-12-21 15:10:55.259349877 +0800 CST :recv false. continue2016-12-21 15:10:56.261039127 +0800 CST :recv false. continue2016-12-21 15:10:57.262770422 +0800 CST :recv false. continue2016-12-21 15:10:58.264534647 +0800 CST :recv false. continue2016-12-21 15:10:59.265680422 +0800 CST :recv true. return

And example1 are not two.

3, Example3

Now the sending behavior of the producer routine has changed: from the previous time every 1s sent data into every 7s sent data, and consumer routine unchanged:

//example3.go//producer routine    go func() {        for i := 0; i < 10; i++ {            time.Sleep(time.Second * 7)            c <- false        }        time.Sleep(time.Second * 7)        c <- true    }()

Let's take a look at the results of Example3.go:

$go run example3.go2016-12-21 15:14:32.764410922 +0800 CST :timer expired

Program Hang up. Can you guess where the hang is? Yes, it was at the time of drain t.c hang:

           // timer may be not active and may not fired            if !timer.Stop() {                <-timer.C //drain from the channel            }            timer.Reset(time.Second * 5)

The send behavior of the producer has changed, Comsumer routine has a time expire event before the first data is received, and the For loop returns to the beginning of the loop. At this point the timer. The STOP function returns no longer true, but false, because the timer has been expire, the timer has not been included in the minimum heap, stop cannot find the timer in the smallest heap, and returns false. So the Example3 code tries to drain (drain) the timer. The data in C. But the timer. C In this time there is no data, so routine block on the channel recv.

In previous versions of Go 1.8, many people encountered similar problems and proposed issue, such as:

time: Timer.Reset is not possible to use correctly #14038

However, go team thought that this was not enough to describe the use of reset in the documentation, so in go 1.8, the Reset method of the document was supplemented, go 1.8 Beta2 in the Reset method of the document changed to:

Resetting a timer must take care not to race with the send into t.C that happens when the current timer expires. If a program has already received a value from t.C, the timer is known to have expired, and t.Reset can be used directly. If a program has not yet received a value from t.C, however, the timer must be stopped and—if Stop reports that the timer expired before being stopped—the channel explicitly drained:if !t.Stop() {        <-t.C}t.Reset(d)

The general meaning is: If definite time is already expired, and T. C has been emptied, then reset can be used directly, if the program has not previously been from T. c Read the value, you need to first call stop (), if it returns true, indicating that the timer has not expire,stop successfully deleted the timer, can be directly reset; If you return false, it indicates that the stop was expire and requires an explicit drain Channel

4, Example4

Our example3 is "time has expired, and T." C has been emptied, so you can use reset directly "in this first case, we should direct reset without explicitly drain the channel. The direct idea of how to combine these two scenarios is to add a switch variable, ischanneldrained, to identify the timer. C has been emptied, and if empty, the reset is called directly. If not, the Channel is drain.

Adding a variable is always troublesome, Russcox also gives an untested method, let's take a look at the example4.go that is modified in this way:

Example4.go//consumer go func () {//try to read from channel, block at most 5s.        If timeout, print time event and go on loop.        If read a message which is isn't the type we want (we want true, not false),//Retry to read. Timer: = time. Newtimer (time. Second * 5) for {//timer is not active, and fired if!timer. Stop () {select {case <-timer. C://try to drain from the channel Default:}} timer. Reset (time. Second * 5) Select {case b: = <-c:if b = = False {fmt. Println (time. Now (), ": recv false. Continue ") Continue}//we want true, not false fmt. Println (time. Now (), ": recv true. Return ") return case <-timer. C:fmt. Println (time. Now (), ": Timer expired") cOntinue}}} () 

Execution Result:

$go run example4.go2016-12-21 15:38:16.704647957 +0800 CST :timer expired2016-12-21 15:38:18.703107177 +0800 CST :recv false. continue2016-12-21 15:38:23.706665507 +0800 CST :timer expired2016-12-21 15:38:25.705314522 +0800 CST :recv false. continue2016-12-21 15:38:30.70900638 +0800 CST :timer expired2016-12-21 15:38:32.707482917 +0800 CST :recv false. continue2016-12-21 15:38:37.711260142 +0800 CST :timer expired2016-12-21 15:38:39.709668705 +0800 CST :recv false. continue2016-12-21 15:38:44.71337522 +0800 CST :timer expired2016-12-21 15:38:46.710880007 +0800 CST :recv false. continue2016-12-21 15:38:51.713813305 +0800 CST :timer expired2016-12-21 15:38:53.713063822 +0800 CST :recv true. return

We use a select to wrap the channel drain so that drain does not block if there is data in the channel. It seems that the problem is solved.

5. Competitive conditions

If you've seen Timerproc's code, you'll find one of those codes:

// go1.7// $GOROOT/src/runtime/time.go            f := t.f            arg := t.arg            seq := t.seq            unlock(&timers.lock)            if raceenabled {                raceacquire(unsafe.Pointer(t))            }            f(arg, seq)            lock(&timers.lock)

We see that before Timerproc executes the function f (ARG, seq), Timerproc unlock the Timers.lock, that is, the execution of F is not within the lock.

As I said earlier, what is the implementation of F?

For Afterfunc, it is to start a goroutine and execute the user-passed function in the new Goroutine;
For the timer created by the Create posture after and Newtimer, the execution of F is the execution of Sendtime, which is to T. The current time of the Send in C.

Note: At this time the timer expire process Sendtime execution and "drain channel" is executed in two Goroutine respectively, who first who after, completely rely on runtime scheduling. So the seemingly no-problem code in Example4.go may also have problems (of course, it takes time granularity to be small enough, such as the MS-level timer).

If the execution of the sendtime occurs before the drain channel executes, then it is the execution result in Example4.go: Stop returns False (because the timer has been expire), the explicit drain channel will read the data out, Following reset, the timer executes normally;
If the execution of the sendtime occurs after the drain channel executes, then the problem comes, although stop returns False (because the timer is already expire), but the drain channel does not read any data. The sendtime then sends the data to the channel. The channel in the timer after reset has actually had the data, so when entering the following select execution, "Case <-timer." C: "Instantaneous return, triggering the timer event, there is no start time-out wait for the role."

This is also the ISSUE:*TIME:TIMER.C can still trigger even after Timer.reset is called #11513中问到的问题.

This is also described in the Go official documentation:

Note that it is not possible to use Reset's return value correctly, as there is a race condition between draining the channel and the new timer expiring. Reset should always be invoked on stopped or expired channels, as described above. The return value exists to preserve compatibility with existing programs.

Do you really have the correct use posture of the Reset method?

In combination with the above examples and analysis, the use of reset does not seem to be the ideal solution, but in general, under the specific business logic, reset can still work, as example4. Even if there is a problem, if you understand the principle behind reset, the problem will be solved quickly and accurately.

The relevant code in this article can be downloaded here.

Iv. references

Golang official about the timer issue list:

Runtime:special Case Timer Channels #8898
Time:timer stop, how to use? #14947
Time:document proper usage of timer.stop #14383
*time:timer.reset isn't possible to use correctly #14038
Time.after doesn ' t release memory #15781
Runtime:timerproc does not get to run under load #15706
Time:time. After uses memory until duration times out #15698
Time:timer Stop Panic #14946
*TIME:TIMER.C can still trigger even after Timer.reset is called #11513
Time:Timer.Stop documentation incorrect for Timer returned by Afterfunc #17600

Related information:

    1. Timers Timer in Go
    2. Go internal implementation of the timer
    3. Go timer
    4. How does they do it:timers in Go
    5. How accurate a timer can be in Go

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.