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.
댓글 영역