상세 컨텐츠

본문 제목

Section 5.2.24 Timers and Tickers

Go/Mastering Go

by Gopythor 2022. 3. 5. 18:17

본문

728x90
반응형

Durations

Number of nanoseconds between two times instances
Go's way to identify time durations
Examples:
3*time.Second => time duration of 3 seconds
3*time.Minute => time duration of 3 minutes

  • So what are the relations.
  • The Duration is a special type in go that can describe a time duration based on the number of nanoseconds.
  • Between the start and the end time the type exists in the time package
  • Let's visit the time package(https://pkg.go.dev/time).
  • Let's look at the duration type.
const (
  Nanosecond Duration = 1
  Microsecond = 1000 * Nanosecond
  Millisecond = 1000 * Microsecond
  Second = 1000 * Millisecond
  Minute = 60 * Second
  Hour = 60 * Minute
)
  • There are constants in here
  • The present different time units like seconds minutes hours and so forth.
  • To create a duration, We use one of these constants multiplied by the number of units representing the time duration.
  • seconds :=10 fmt.Println(time.Duration(seconds)*time.Second)// prints 10s
  • So for example here 10 seconds.
  • So we typecast the number 10 to time.Duration that duration and then multiply by the time.Second constant.
  • And that will give us the Go Language representation of 10 second duration..
package main

import (
    "fmt"
    "time"
)

func main() {
    go SlowCounter(2)
    time.Sleep(15 * time.Second)
}

func SlowCounter(n int) {
    i := 0
    // create a duration of n seconds
    d := time.Duration(n) * time.Second

    for {
        // create a timer for this duration
        t := time.NewTimer(d)
        <-t.C
        i++
        fmt.Println(i)
    }
}
  • Let's explore some code to showcase the dration concept.
  • So we'll will create a slow counter that does number counting but slowly.
  • So there will be a delay of n seconds between each count and the next.
  • So we'll take n as an argument and then we will create a duration which we'll present n in seconds so we typecast n to time.Duration and then we multiply by time.Second.
  • So We'll also initialize our counter to zero.

Time

type Timer struct {
    C <-chan Time
    // contains filtered or unexported fields
}
  • Now let's explore the Timer type.
  • This type is also in the time package.
  • It gives us access to a go channel that triggers in the future.
  • This allows us to present a future event as we can listen to this channel and take an action when it triggers.
  • Let's see how we can use it.
  • Let's create a timer and use it in our slow counter.
  • to create a timer, Use time.NewTimer call which d is time duration as an argument.
  • So as a reminder our time duration was n in seconds.
  • We can then wait on the timer channel which is named capital C.
  • And this will freeze our go routine till the time duration was specifed elapses
  • After Elapses we can go ahead and do our increment and then we print it.
  • In order to have this functionality repeated indefinitely, We use a for loop as shown
  • And by doing this we created a scheduled timer that runs every n seconds.
  • And performs a specific task which is to increment a number then print it.
  • Now when our main function, we'll call this SlowCounter with the number two in a separate go routine.
  • The other go keyword will sleep the main thread for 15 seconds to give an opportunity for the other go routine to run a bit.
  • Now run our code
    https://go.dev/play/p/XBpuGFX_Ttp
  • See here that we are getting a count every about 2 seconds.
  • The timer pointer type support two methods reset and stop.
  • both methods are effective before the timer channels fires??? which means they have to be called Between the time the timer gets created.
  • And the timer channel sends a value or in other words fires or triggers.Stop
  • Stop will stop the timer effectively acting as a cancel for that timer.

Reset

  • Reset on the other hand can be used to change that dration of the timer if you want to.
package main

import (
    "fmt"
    "time"
)

func main() {
    go SlowCounter(2)
    time.Sleep(15 * time.Second)
}

func SlowCounter(n int) {
    i := 0
    // create a duration of n seconds
    d := time.Duration(n) * time.Second

    for {
        // create a timer for this duration
        <- time.After(d)
        i++
        fmt.Println(i)
    }
}

https://go.dev/play/p/ypeMSuR2zD0

  • Let's explore another approach to create a schaduled timer.
  • We'll use timer.After channel which we will only fire after duration elapses.
  • This in effect provide similar functionality as before
  • but with a single line of code
  • so the code called time.After.
package main

import (
    "fmt"
    "time"
)

func main() {

    nc := make(chan int)
    stopc := make(chan bool)

    go SlowCounter(1, nc, stopc)
    time.Sleep(5 * time.Second)

    nc <- 2
    time.Sleep(6 * time.Second)
    stopc <- true
    time.Sleep(1 * time.Second)
}

func SlowCounter(n int, nc chan int, stopc chan bool) {
    i := 0
    // create a duration of n seconds
    d := time.Duration(n) * time.Second

    for {
        select {
        // Use time.After channel to wait for a time period
        case <-time.After(d):
            i++
            fmt.Println(i)
        case n = <-nc:
            fmt.Println("Timer duration changed to", n)
            d = time.Duration(n) * time.Second
        case <-stopc:
            fmt.Println("Timer stopped")
            break
        }
    }
    fmt.Println("Existing Slow Counter")
}

https://go.dev/play/p/ZJQDz6R1_3t

  • What if we want to have more control over our scheduled timer.
  • For example let's say we want to change the duration or cancel the timer from outside the GO routine.
  • We will create two channels for this one for the duration.
  • We'll call it n channel.
  • So that is a duration as an integer.
  • The other one for a stop signal so boolean channel.
  • That we will use as a signal to cancel the timer.
  • We'll Change the SlowCounter function to take the two channels as arguments
  • After that we use a select statements in order to handle the different channels that we need to support.
  • The first channel will be a time.after which will only trigger after duration elapses.
  • The second channel will be the new number for our duration in number of seconds.
  • So if we receive this number, we will log this fact on the standard output and then we will update our duration as shown.
  • The third channel is our stop signal so when we'll receive it, We should break out of the loop.
  • In go, However the break statement inside the select statement is not enough to break out of the surrounding for loop.
  • It's only enough to break out of the select statements.
  • |n order to break out of the enclosing for loop, we will need to use what is called a label.

A loop label

func SlowCounter(n int, nc chan int, stopc chan bool) {
    i := 0
    // create a duration of n seconds
    d := time.Duration(n) * time.Second
Loop:
    for {
        select {
        // Use time.After channel to wait for a time period
        case <-time.After(d):
            i++
            fmt.Println(i)
        case n = <-nc:
            fmt.Println("Timer duration changed to", n)
            d = time.Duration(n) * time.Second
        case <-stopc:
            fmt.Println("Timer stopped")
            break Loop
        }
    }
    fmt.Println("Existing Slow Counter")
}

https://go.dev/play/p/UcOtCBV1M_3

  • So a label can be written like this.
  • So create a label called loop as shown here.
  • So a label will sit on top of the for loop.
  • The label will end with a semicolon and then our break statement was changed to indicate that it's breaking out of the label.
  • Now parts for a break statement.
  • So the syntax will cause the Select Case to break out of the in casing for loop.
  • Now in our main code will call SlowCounter on a different Go routine with a 1 second duration and will pass two channels nc and stopc will then sleep for 5 seconds to give some opportunity for the code to run.
  • Then we'll try to change the duration to 2 seconds by passing the number 2 to our NC channel.
  • We'll then wait 6 seconds just for that code to run for a little while.
  • Before we cancel the timer for good, by passing true to the stop channel.For good : 영원히, 영구히
  • We'll then sleep for one second.
  • In order to test out the scheduled timer had really stopped.
  • So let's see.
  • If we run our code, we see now that the counter is working with the 1 second interval and now we changed 2.
  • Now it's actually slower.
  • And then Stops and then we get a message saying that it's exiting
  • ???? gives us the flexibility to control our timers how often they run and when to stop them.
  • type Ticker struct { C <- chan Time // The channel on which ticks are delivered. // contains filtered or unexported fields }
  • Another way to execute code in scheduled intervals is via the Ticker type.
  • It's also found in the time package.
  • The ticker type will provide us a channel that will fire a signal periodically.
  • Every time duration that we specify beforehand when we create a ticker.
  • That channel is of type Time.
  • The value to turns???? is the time at which trigger occures.
  • Let's see how we can use that in code.
package main

import (
    "fmt"
    "time"
)

func main() {
    go tickCounter(1)
    time.Sleep(5 * time.Second)
}

func tickCounter(n int) {
    ticker := time.NewTicker(time.Duration(n) * time.Second)
    i := 0
    for t := range ticker.C {
        i++
        fmt.Println("Count ", i, " at ", t)
    }
}

https://go.dev/play/p/e9fOJaAK1Ah

  • We'll write the counter as before but this time we'll use a ticker.
  • First, we will create a NewTicker.
  • The NewTicker will take the time.Duration that control the priodic.
  • Ticker channel fires that occure.
  • So the time duration will be end in seconds .
  • So same technique as before.
  • When it comes to the duration then initialize our counter to zero and then we use a for loop.
  • Use the range keyword, In order to keep listening to the channel.
  • We will output the channel received value to variable t.
  • And then inside our for loop, we will increment our counter and then we will log.
  • We are Our counter is "at" and the value of t which again is the value received from the ticker channel.
  • The main function we will create a tickCounter every one second.
  • Putted on different Go routines and then we will sleep for five seconds.
  • In order to get the go routine to run for a little bit.
  • So let's run our code.
Count  1  at  2009-11-10 23:00:01 +0000 UTC m=+1.000000001
Count  2  at  2009-11-10 23:00:02 +0000 UTC m=+2.000000001
Count  3  at  2009-11-10 23:00:03 +0000 UTC m=+3.000000001
Count  4  at  2009-11-10 23:00:04 +0000 UTC m=+4.000000001
  • We will see here that our code is running every second.
  • The data here is not correct and that is because the Go playground currently runs assuming a this time step.
  • But you can see here that there is a 1 second difference.
  • Between our counts which proves that this channel fired every second as we requested.
package main

import (  
"fmt"  
"time"  
)

func main() {  
    go tickCounter(2)  
    time.Sleep(5 \* time.Second)  
}

func tickCounter(n int) {  
    ticker := time.NewTicker(time.Duration(n) \* time.Second)  
    i := 0  
    for t := range ticker.C {  
    i++  
    fmt.Println("Count ", i, " at ", t)  
    }  
}
Count 1 at 2009-11-10 23:00:02 +0000 UTC m=+2.000000001  
Count 2 at 2009-11-10 23:00:04 +0000 UTC m=+4.000000001

https://go.dev/play/p/78OiUUiSu2j

  • So for change that to two and run again, now it's changing every two seconds.

Stop the ticker from the outside.

package main

import (  
"fmt"  
"time"  
)

func main() {  
    ticker := time.NewTicker(1 * time.Second)  
    go tickCounter(ticker)  
    time.Sleep(5 * time.Second)  
    ticker.Stop()  
    time.Sleep(10 * time.Second)  
    fmt.Println("Exting...")  
}

func tickCounter(ticker \*time.Ticker) {
    i := 0
    for t := range ticker.C {
    i++
    fmt.Println("Count ", i, " at ", t)
    }
}

https://go.dev/play/p/iqmdpCBB_CB

  • What if you want to stop the ticker from the outside.
  • In this case, define the ticker, outside of the tickCounter function then pass a ticker to the function instead of just n.
  • so time.NewTicker returns a pointer to a ticker which will store it in a variable called ticker and a pointer to ticker is what implements a channel that fires every time duration that was specified.
  • One important remark is that we can multiply time.Second directly without number to specify a duration without needing to do time testing to time.Duration.
  • So this only works if we use the actual number.
  • But if we use a variable here(ticker := time.NewTicker(1 * time.Second)), that we need to do the typecasting time.Duration.
  • Now back to our ticker type, we have access now to our ticker type in our main function.
  • So now we can sleep for five seconds after we call the tickCounter on the different Go routine.
  • And then we can try to stop the ticker from our main function by calling ticker.Stop.
  • Then let's wait for about 10 seconds and then we will exits.
Count 1 at 2009-11-10 23:00:01 +0000 UTC m=+1.000000001  
Count 2 at 2009-11-10 23:00:02 +0000 UTC m=+2.000000001  
Count 3 at 2009-11-10 23:00:03 +0000 UTC m=+3.000000001  
Count 4 at 2009-11-10 23:00:04 +0000 UTC m=+4.000000001  
Count 5 at 2009-11-10 23:00:05 +0000 UTC m=+5.000000001  
Exting...

Program exited.
  • So if I hit run, we will see that we're working normally now.
  • So counting every second as requested and now is stopped and that is because the ticker had stopped.
  • Then 10 seconds later it exits.
  • Our code is functioning as expected.

Difference between a timer and the ticker.

  • You were now be wondering what the difference is between a timer and the ticker.
  • There is actually an important difference between the two that we need to know in order to properly design software in Go.
  • func SlowCounter(n int) { i := 0 // create a duration of n seconds d := time.Duration(n) * time.Second for { /* something conplex that takes time */ // create a timer for this duration t := time.NewTimer(d) <-t.C i++ fmt.Println(i) } }
  • Timer
  • In case of a timer, you control when the time duration waits starts.
  • Because you declare a new timer or you just use time.After and then you wait on the channel of the timer.
  • So let's say we're doing some code that is complex and takes some time to execute.
  • When we use a timer we learned time Duration wait will happen after the complex execution finishes.
  • So we'll try to execute code here so it will execute but it will take time.
  • We then declare a timer and wait on the timer so that we can guarantee the time duration started from here and then we do whatever we need to do.
  • And then the fou recycles again.

Ticker

func tickCounter(ticker \*time.Ticker) {
    i := 0
    for t := range ticker.C {
    i++
    fmt.Println("Count ", i, " at ", t)
    }
}
  • In case of the ticker on the other hand the time Duration wait only happens based on the last ticker channel fire occurred.
  • So if we have a piece of code here that is complex and takes time to execute.
  • Our ticker will not wait for it.
  • Before it starts a new time with duration.
  • What will happen is say if the ticker fires every second and this piece of code takes five seconds to execute.
  • Then right after the this piece of code is done.
  • And since we're not running it on a separate Go routine, there will be another channel fire ready on the pipe line.
  • So the for will execute again right after.
  • It will not wait like in the timer case.
  • So that's a difference between a timer and a ticker.

Another important remark

  • Another important remark about tickers is that the ticker.stop called does not close that ticker channel.
  • So even though the channel stops firing if we have a for loop with the range keyword of that channel,
  • It will never exit.
  • The stop does not close the channel so that if a go routine is listening or reading from the channel will not think that it succeeded.
  • So the implication of this is that it this for Loop where we listen to a ticker via range keyword.
  • Stopping the ticker will not close channel.
  • Meaning that this Go routine will never exit properly which we should prevent.
  • So how to make our ticker counter go routine truly exits,
  • We can use another channel to signal that this Go routine needs to exit.
func main() {
    ticker := time.NewTicker(1 * time.Second)
    done := make(chan bool)
    go tickCounter(ticker, done)
    time.Sleep(5 * time.Second)
    ticker.Stop()
    done <- true
    time.Sleep(10 * time.Second)
    fmt.Println("Exting...")
}

func tickCounter(ticker *time.Ticker, done chan bool) {  
    i := 0  
Loop:  
    for {  
        select {  
            case t := <-ticker.C:  
                i++  
                fmt.Println("Count ", i, " at ", t)  
            case <-done:  
                fmt.Println("done signal")  
                break Loop  
                }  
        }  
    fmt.Println("Exiting the tick counter")  
}
Count 1 at 2009-11-10 23:00:01 +0000 UTC m=+1.000000001  
Count 2 at 2009-11-10 23:00:02 +0000 UTC m=+2.000000001  
Count 3 at 2009-11-10 23:00:03 +0000 UTC m=+3.000000001  
Count 4 at 2009-11-10 23:00:04 +0000 UTC m=+4.000000001  
Count 5 at 2009-11-10 23:00:05 +0000 UTC m=+5.000000001  
done signal  
Exiting the tick counter  
Exting...
  • Let's see how
  • so we create another channel called done of type boolean.
  • And then we passed it here to the tickCounter as an argument.
  • Then after we stopped the ticker, we send a signal on the done channel.
  • Then inside our tickerCounter function will change the code a bit to make that work.
  • So We will change our for loop to include a select statement.
  • First case is if the ticker fires like this.
  • So the second case will be if done channel fires.
  • So if done channel fires, will print that we received the signal just for us to know where our code is going
  • And then we will need to do a break for the for loop.
  • So as mentioned earlier we can create a label called loop and then our print statement will include that label.
  • And then after our for loop will print the fact that we are exiting the tickCounter.
  • So if I hit run now, we will start running as usual saying that when it exits it will actually exit this function.
  • Because this was triggered.
  • And now it's exiting the main function as well.
  • So we exited our go routines cleanly.

Summary

  • We discussed several ways to schedule the execution of code in the future.
  • We now have one more powerful tool in our tool box to build efficient production ready software in the programming language.
728x90
반응형

관련글 더보기

댓글 영역