Task scheduling involves setting up the tasks and jobs that can run at specific times or intervals. It also involves repetitive processing which ensures efficient resource utilization.
In this tutorial, we will learn the essentials for setting and building the task scheduling apps using the Go programming language.
What Is Task Scheduling?
Task scheduling refers to the process of planning and executing the tasks or jobs at specific times or in response to particular events.
Task scheduling is fundamental in various domains including system administration, data processing, web applications, and more.
In Go, we can work with task scheduling mechanism using goroutines which are essentially lightweight threads of execution which can run the tasks in a concurrent manner.
Types of Task Scheduling
We can categorize the task scheduling into various groups based on the criteria for execution. Such task scheduling tasks include:
- Time-Based Scheduling – This refers to tasks that are scheduled to run at specific times or intervals. An example can periodic backups, cron jobs, etc.
- Event-Driven Scheduling – Event driven tasks refer to tasks that are triggered by specific tasks or conditions such as sensor reading, message from external system, etc.
- Priority-Based Scheduling – We have priority-based scheduling where tasks with a higher-priority are executed first before the lower-priority tasks.
- Dependency-Based Scheduling – In dependency-based scheduling, tasks have dependencies on other tasks and they are executed in a sequence that satisfies these dependencies.
With that out of the way, let us discuss about task scheduling in Go.
Go Concurrency
One of the most renowned features of the Go programming language is its excellent support for concurrency which makes it very efficient for building highly concurrent apps.
There are two main primitives in the Go language to provide support for concurrency: goroutines and channels.
Goroutines
A goroutine is a lightweight thread of execution that is managed by the Go runtime. Goroutines provide a more efficient and safe features compared to traditional threads.
In Go, we can create a goroutine using the “go” keyword before a function call as shown in the following:
go func()
// ...
}
This should create a goroutine that executes the function called “func” in a concurrent manner.
Channels
Channels, on the other hand, are very powerful mechanism that provide communication and synchronization between goroutines.
The role of channels is to provide the means of safe data sharing and synchronization without the need to use locks.
In Go, we can create a channel using the “chan” keyword. From there, we can use the channels for sending or receiving the data between the routines.
This should create an integer channel.
Task Scheduling in Go
Now that we have a basic understanding of the concurrent features of Go, let us learn how to use them for task scheduling.
Using Goroutines and Channels
The most basic and strong foundation for a task scheduler in Go involves us using the goroutines and channels.
We can create goroutines to execute the tasks concurrently and use channels to communicate between them.
Consider a basic example as shown in the following:
import (
"fmt"
"time"
)
func task(name string) {
time.Sleep(5 * time.Second)
fmt.Println("Task", name, "completed.")
}
func main() {
tasks := []string{"Task1", "Task2", "Task3", "Task4"}
for _, taskName := range tasks {
go task(taskName)
}
time.Sleep(8 * time.Second)
}
In this example, we have goroutines that execute the tasks concurrently. We also use the time.Sleep() function with a duration of five seconds to simulate a task’s work.
The program then waits for all the tasks to complete using the time.Sleep().
Timer-Based Scheduling
Timer-based scheduling involves executing the tasks at specific times or after a certain duration. Go provides the time package to work with timers.
Consider the following example that demonstrates how to schedule a task to run after a delay:
import (
"fmt"
"time"
)
func task() {
fmt.Println("Task executed at:", time.Now())
}
func main() {
delay := 3 * time.Second
timer := time.NewTimer(delay)
<-timer.C
task()
}
In this example, we start by creating a timer with a delay of three seconds using the time.NewTimer() function.
We then wait for the timer to expire using the “<- timer.C” and execute the task when the task triggers.
You can check our detailed tutorial on Golang timers.
Periodic Scheduling
We can also create a periodic task schedule in Go using a combination of goroutines and tickers which allows a task to run at a fixed interval as shown in the following example:
import (
"fmt"
"time"
)
func task() {
fmt.Println("Task executed at:", time.Now())
}
func main() {
interval := 2 * time.Second
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
task()
}
}()
time.Sleep(5 * time.Second)
ticker.Stop()
}
From the given example, we start by creating a “time.Ticker” ticker that triggers a task at regular intervals. We also setup a goroutine to continuously wait for the ticker event and run the specified task.
Task Priority and Dependency Management
When dealing with task priority and dependency-based task scheduling, it is good to be careful as there lies a potential for screw ups.
However, in Go, we can implement the priority and dependency-based scheduling with data structures such as queues and priority queues.
Luckily, the Go standard library provides us the container/heap package to manage the priority queues.
Consider the following example that demonstrates how to configure a priority-based scheduling in Go:
import (
"container/heap"
"fmt"
)
// scheduled task with a name and priority.
type Task struct {
Name string
Priority int
}
// priority queue of tasks.
type TaskQueue []Task
func (pq TaskQueue) Len() int { return len(pq) }
func (pq TaskQueue) Less(i, j int) bool { return pq[i].Priority > pq[j].Priority } // Higher priority first
func (pq TaskQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }
func (pq *TaskQueue) Push(x interface{}) {
item := x.(Task)
*pq = append(*pq, item)
}
func (pq *TaskQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[0 : n-1]
return item
}
func main() {
tasks := TaskQueue{
{Name: "Task A", Priority: 3},
{Name: "Task B", Priority: 1},
{Name: "Task C", Priority: 2},
}
heap.Init(&tasks)
for tasks.Len() > 0 {
task := heap.Pop(&tasks).(Task)
fmt.Printf("Executing %s (Priority: %d)\n", task.Name, task.Priority)
}
}
In the given example, we start by defining a “Task” struct which contains the name and priority fields.
We also define a “TaskQueue” as priority queue. We then proceed to push the tasks into the queue along with their corresponding priorities.
Lastly, we use the container/heap package to manage the task priorities. This allows the tasks to run depending on their level of priorities.
Conclusion
In this tutorial, we introduced you to one of the most powerful and useful aspect in the development of task scheduling. We covered how to work with the powerful concurrency primitives of Go like goroutines and channels to set up the task scheduling.