When it comes to performance and efficiently utilizing the available resources in modern systems, Go is unparalleled. Ask Docker.
When it comes to concurrent applications, one of the challenges that rise more than any other is managing the access to shared resources. Luckily, we have this thing that has been around for ages known as semaphores which can help to managing the access to a shared resource among multiple threads or goroutines.
In this tutorial, we will explore the workings and implementation of semaphores in Go. We will start with what semaphores are and then provide examples on how to create and use them in Go.
What Are Semaphores?
Let us start with the fundamentals.
A semaphore is a powerful and useful synchronization mechanism that maintains a count and two operations:
- A wait
- A signal
In semaphore, a count is basically a number that denotes the number of resources.
When a specific goroutine wants to access the shared resource (protected by the semaphore), the goroutine needs to call “Wait”. If there are any available resources, the “Wait” operation decrements the count of resource and allows the goroutine to process.
However, if the count of the resources reaches zero, any subsequent “Wait” calls will block any goroutines from proceeding until the resources becomes available.
Once a goroutine finishes the task with the resource, the “Signal” operations handles the task of incrementing the count to indicate that the resource is available to be acquired by another goroutine.
Create a Semaphore in Go
In Go, we can create a semaphore using a buffered channel. A buffered channel of size “N” can act as a semaphore with “N” number of resources.
Take a look at the following code:
func main() {
semaphore := make(chan struct{}, 5)
defer close(semaphore)
}
In this example, we create a buffered channel called “semaphore” with the size of 5. This means that it holds five resources.
To release a resource, we can send an empty struct into the channel.
To acquire a resource, we just receive it from the channel.
Semaphore Control Access to a Resource
Of course, when it comes to real-world, you will hardly have a basic semaphore. One of the roles of a semaphore is controlling the resources.
Suppose we have a limited number of resources that multiple goroutines need to access concurrently.
To control the number of goroutines that can access it simultaneously, we can use a semaphore as shown in the following example:
import (
"fmt"
"sync"
func worker(id int, semaphore chan struct{}) {
fmt.Printf("Worker %d is waiting for a permit\n", id)
<-semaphore
defer func() {
fmt.Printf("Worker %d has released the permit\n", id)
semaphore <- struct{}{}
}(
fmt.Printf("Worker %d is working\n", id)
}
func main() {
semaphore := make(chan struct{}, 5)
for i := 0; i < 5; i++ {
semaphore <- struct{}{}
}
defer close(semaphore)
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(id, semaphore)
}(i)
}
wg.Wait()
}
In the given example, we declare a worker function which simulates a task that is executed by a goroutine. The function accepts two parameters: an id to identify the worker and a semaphore which is the channel that controls the access to the resource.
In the function, we have the “send” operation to the channel which is denoted by <- semaphore. If the channel is empty, the worker will block and wait. It waits until it receives a resource from the channel.
We also setup a deferred function that executes when the function returns. Once done, it sends an empty struct to the channel to release the resource.
Running the given code returns the following:
Worker 4 is waiting for a permit
Worker 4 is working
Worker 4 has released the permit
Worker 3 is waiting for a permit
Worker 3 is working
Worker 1 is working
Worker 1 has released the permit
Worker 5 is waiting for a permit
Worker 5 is working
Worker 5 has released the permit
Worker 3 has released the permit
Worker 2 is waiting for a permit
Worker 2 is working
Worker 2 has released the permit
This shows a very good example of a semaphore controlling shared resources to multiple goroutines that attempt to access and use the resources concurrently.
Conclusion
In this tutorial, we learned about one of the most powerful constructs when it comes to concurrency: semaphore using channels in the Go programming language. Be careful with deadlocks like with everything related to concurrency.