Parallel Function Execution in Go Using Concurrency
Introduction
As part of my exploration of Golang, I came across a popular feature: first-class support for concurrency. I believe we all understand the benefit or importance of concurrency. In the HTTP way, when an endpoint needs to fetch data from multiple upstreams, aggregate the data and produce it as a response, Go concurrency helps to reduce the latency for that API request. Two features in Go, goroutines and channels make concurrency easier when used together.
Goroutines example: Run functions in parallel
Modern computers are equipped with processors, or CPUs, designed to efficiently handle multiple streams of code simultaneously. These processors are built with one or more "cores," each capable of running one code stream at a given time. To fully utilize the speed boost multiple cores offer, programs must be able to split into various streams of code. This division can be challenging, but Go was explicitly developed to simplify this process.
Go achieves this through a feature known as goroutines, special functions that can run alongside other goroutines. When a program is built to execute multiple streams of code simultaneously, it operates concurrently. Unlike traditional foreground operations, in which a function runs to completion before the following code executes, goroutines allow for background processing, enabling the following code to run while the goroutine is still active. This background operation ensures that the code doesn't block other processes from running.
Goroutines provide the advantage of running on separate processor cores simultaneously. For instance, if a computer has four processor cores and a program has four goroutines, all four can run concurrently. This simultaneous execution of multiple code streams on different cores is called parallel processing.
Jumping into the example, create a multifunc
directory named go-concurrency-project
.
mkdir go-concurrency-project
cd go-concurrency-project
Once you’re in the go-concurrency-project
Directory, open a file named main.go
using nano
, or the editor of your choice:
nano main.go
Add the following code to the main.go
file,
package main
import (
"fmt"
)
func make(total int) {
number := 0
for number < total {
number = number + 1
fmt.Printf("Generated number %d\n", number)
}
}
func print() {
number := 0
for number < 2 {
number = number + 1
fmt.Printf("Print: number %d\n", number)
}
}
func main() {
print()
make(2)
}
Based on the above setup, make
and print
Functions are structured to run in sequence. make
Accepts a number to generate up to and prints only five numbers.
This is how it will look like when we execute main.go
,
go run make.go
// Output
Print: number 1
Print: number 2
Generated number 1
Generated number 2
If you notice, the function printed the output in sequence based on its execution pattern.
When running two functions synchronously, the program takes the total time for both functions to run. But if the functions are independent, you can speed up the program by running them concurrently using goroutines, potentially cutting the time in half. To run a function as a goroutine, use the go
keyword before the function call. However, you need to add a way for the program to wait until both goroutines have finished running to ensure they all complete running.
To synchronize functions and wait for them to finish in Go, you can use a WaitGroup
from the sync
package. The WaitGroup
primitive counts how many things it needs to wait for using the Add
, Done
, and Wait
functions. The Add function increases the count, Done
decreases the count, and Wait
can be used to wait until the count reaches zero.
To do that update main.go
,
package main
import (
"fmt"
"sync"
)
func make(total int, wg *sync.WaitGroup) {
defer wg.Done()
number := 0
for number < total {
number = number + 1
fmt.Printf("Generated number %d", number)
}
}
func print(wg *sync.WaitGroup) {
defer wg.Done()
number := 0
for number < 2 {
number = number + 1
fmt.Printf("Print: number %d", number)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go print(&wg)
go make(2, &wg)
fmt.Println("Awaiting....")
wg.Wait()
fmt.Println("Done!")
}
After declaring WaitGroup
, specify how many processes to wait for. In the example, the goroutine
waits for two Done
calls before finishing. If not set before starting the goroutines, things might happen out of order, or the code may panic because wg
doesn't know if it should wait for any Done
calls.
Each function will use defer
to call Done
, which decreases the count by one after the function finishes. The main
function is updated to include a call to Wait
on the WaitGroup
. This ensures that the main
function waits until both functions call Done
before continuing and exiting the program.
After saving your main.go
execute the file,
go run main.go
// Output
Awaiting....
Generated number 1
Generated number 2
Print: number 1
Print: number 2
Done!
Your output may vary each time you run the program. With both functions running concurrently, the output depends on how much time Go and your operating system allocates to each function. Sometimes, each function runs entirely, and you'll see their complete sequences. Other times, the text will be interspersed.
Conclusion
If you’re interested in learning more about concurrency in Go, the Effective Go document created by the Go team provides much more detail. The Concurrency is not parallelism Go blog post is also an exciting follow-up about the relationship between concurrency and parallelism. These two terms are sometimes mistakenly thought to mean the same thing.
Thank you for reading this article! If you're interested in DevOps, Security, or Leadership for your startup, feel free to reach out at hi@iamkaustav.com or book a slot in my calendar. Don't forget to subscribe to my newsletter for more insights on my security and product development journey. Stay tuned for more posts!
Subscribe to my newsletter
Read articles from Kaustav Chakraborty directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Kaustav Chakraborty
Kaustav Chakraborty
Hey, Thanks for taking the time to read this. I'm a software enthusiast with over 10 years of experience in crafting software and organization. I thrive on taking on new challenges and exploring innovative ideas. When I'm not busy coding, I love to travel the world and have already visited 10 countries with 4 more on my upcoming list. I'm also passionate about discussing music, life, and new ideas. If you need someone to listen to your innovative idea, don't hesitate to buzz me. I'm always open to collaborating and lending an ear. With my passion for creativity and my drive to excel, I'm confident that I can help you take your project to the next level. Let's work together to turn your vision into a reality!