Computer scienceProgramming languagesGolangPackages and modulesStandard libraryTime package

Time scheduling

12 minutes read

In Go, the time package provides various functions and types to handle time-related operations and scheduling. Time scheduling is essential in many applications to control the execution of tasks at specific times, intervals, or after a certain duration. It allows developers to handle concurrency, synchronization, and coordination in their applications. Let's explore the common time scheduling features in Go.

Why do we need time scheduling?

Time scheduling is crucial in several scenarios, such as:

  • Synchronizing concurrent tasks: You may need to coordinate the execution of multiple goroutines or processes to ensure they occur in a particular order or at specific intervals.

  • Timeouts: You might want to enforce a maximum duration for an operation, after which it should be canceled or considered unsuccessful.

  • Periodic tasks: You need to execute some tasks repeatedly at fixed intervals or specific times.

  • Delaying execution: In some cases, you may need to pause the execution of code for a certain duration.

Let's dive deeper into functions from the time package that will be an assistant for the points above.

time.Sleep

Imagine you have to pause your program for some time so that background processes are ready. For example, you are working on a traffic simulation, and the car traffic lights should wait for time.Second * 54 after the pedestrian traffic light starts showing green. time.Sleep pauses the current execution and waits till the given time before continuing to work.

// Signature of time.Sleep
func Sleep(d Duration)

Essentially, the time.Sleep function pauses the execution of a goroutine for a specified duration. It takes a time.Duration argument, which represents the duration of sleep. After the specified duration, the goroutine resumes execution.

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Start")
    time.Sleep(2 * time.Second) // Sleep for 2 seconds
    fmt.Println("End")
}

Output:

Start 
// ... 2-second delay
End

It's important to note that time.Sleep is a blocking operation! At first glance, it may seem like the most obvious way to start synchronizing something. However, it's an anti-pattern writing production-ready code because it can cause harm to your applications. Use it carefully to avoid blocking the execution of other goroutines.

time.After

Imaging you have to deal with the following problems:

  • Schedule different tasks;

  • Trying to perform a timeout/event handling;

  • Rate Limiting certain operations;

  • Simulating Behavior;

  • Simulate an AI behavior when developing a game;

  • Dealing with IoT & Sensors;

  • Other similar problems;

How can you crush them all? For these and other scenarios and use cases time.After can be incredibly useful: The key advantage of time.After is its simplicity and ease of use. It returns a channel that will be ready after a specified duration, allowing you to leverage Go's handy select statement to handle timing-related events effectively.

func After(d Duration) <-chan Time

Let's consider a simpler example. The performLongOperation function is designed to take 3 seconds to complete (simulated using time.Sleep). And you are going to run it as goroutine in main()

func performLongOperation(resultChan chan<- string) {
    // Simulate a long operation that takes 3 seconds.
    time.Sleep(3 * time.Second)

    // Send the result to the channel.
    resultChan <- "Operation completed successfully"
}

func main() {
    resultChan := make(chan string)

    // Start the long operation in a goroutine.
    go performLongOperation(resultChan)

// ...

After that, put the select statement that waits for one of two cases to occur:

  1. A response is received successfully from the resultChan.

  2. A timeout of 2 seconds occurs, indicated by the <-time.After(2 * time.Second) case.

// ...

    // Use select to wait for the result or timeout.
    select {
    case result := <-resultChan:
        fmt.Println(result)
    case <-time.After(2 * time.Second):
        fmt.Println("Operation timed out")
    }
}

When the performLongOperation function completes within the timeout (before 2 seconds elapse), the result will be received from the resultChan, and the "Operation completed successfully" message will be printed. However, if the operation takes longer than the timeout (exceeds 2 seconds), the select statement will choose the timeout case, and "Operation timed out" will be printed.

As you see, the time.After function is commonly used for implementing timeouts or triggering actions after a specific delay. The common use case is to set a time limit on the execution of a goroutine and handle cases where the operation takes too long to complete. It's a simple way to enforce a deadline for time-consuming tasks and prevent them from blocking your program indefinitely.

time.Timer

When implementing a reminder or notification service that sends scheduled messages or alerts at specific times, you can use the time.Timer type. It represents a single event that will occur in the future. The timer provides a channel, C, which receives a value when the timer expires.

type Timer struct {
    C <-chan Time
    // contains filtered or unexported fields
}

You can create a Timer using the time.NewTimer function and specify the duration after which the timer should fire. You can use this channel in a select statement to perform specific actions when the timer expires.

func taskA() {
    fmt.Println("Task A executed at", time.Now())
}

func taskB() {
    fmt.Println("Task B executed at", time.Now())
}

func main() {
    timerA := time.NewTimer(3 * time.Second)
    timerB := time.NewTimer(5 * time.Second)

    for {
        select {
        case <-timerA.C:
            taskA()
            // Reset the timer for Task A to run again in 3 seconds.
            timerA.Reset(3 * time.Second)

        case <-timerB.C:
            taskB()
            // Reset the timer for Task B to run again in 5 seconds.
            timerB.Reset(5 * time.Second)
        }
    }
}

Note that you use the Reset method. It allows you to reset the timer to a new duration. By doing so, you can reuse the same time.Timer instance for scheduling the event multiple times.

In this example, taskA will be executed every 3 seconds and taskB will be executed every 5 seconds. When the timerA or timerB expires (reaches its specified duration), the corresponding task will be executed. After each task execution, you reset the respective timer to run again after its specified interval.

If you try to run this program, you'll see the output showing the execution of Task A every 3 seconds and Task B every 5 seconds, as the timers expire and trigger the specific actions in the select statement.

Unlike time.Sleep(), a time.Timer can be stopped with the use of Stop() method. In some scenarios, you want to be able to cancel a process, like if you download a file or if you are trying to connect.

You should always stop the timer, and it's a common practice to use the defer timer.Stop() statement for this. But for learning purposes, you are now viewing it in a code flow.

func main() {
    timer := time.NewTimer(time.Second)
	
    go func() {
        <-timer.C
        fmt.Println("Timer expired")
    }()
	
    stop := timer.Stop()
    if stop {
        fmt.Println("Timer stopped")
    }
}

For a deeper understanding, you could walk through source code of sleep.go

time.Ticker

The last but not least time.Ticker type. It represents a ticker that ticks at regular intervals.

You can create a Ticker using the time.NewTicker function, which takes the interval between ticks as a parameter. The ticker provides a channel, C, that receives values at regular intervals. You can use this channel in a select statement to perform actions periodically.

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Start")
    ticker := time.NewTicker(1 * time.Second)

    go func() {
        for range ticker.C {
            fmt.Println("Tick")
        }
    }()
	
    time.Sleep(5 * time.Second)
    ticker.Stop()
    fmt.Println("End")
}

Just like you do with the Timer, you can stop the Ticker using the Stop() method, and the more idiomatic Go-way is to use the defer statement here as well.

// ...
defer ticker.Stop()

defer timer.Stop()
// ...

In real life, an example of using the time.Ticker type would be a data streaming application from IoT devices. Consider an application that receives real-time data from sensors, IoT devices, or external APIs. To keep the data up-to-date and respond promptly to changes, you want to process the incoming data at regular intervals and take appropriate actions based on the updates.

Conclusion

In this topic, you dived deeper into time scheduling in Go. It allows you to control the execution of tasks based on specific duration, intervals, or time points. The time.Sleep function provides a simple way to pause execution, time.After triggers a channel after a duration, time.Timer fires an event in the future, and time.Ticker repeatedly sends values at regular intervals. These features are useful for managing timeouts, scheduling periodic tasks, and coordinating concurrent operations in Go programs.

9 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo