Go Routines
Goroutines are one of the most important aspects of the Go programming language. They are the smallest unit of execution and can continue their work alongside the main goroutine, creating concurrent execution
. They are lightweight
, cheap to create
, and can be used to effectively utilize multi-core CPUs
. To create a goroutine is simple, just need to add the keyword go
in front of the function you want to run concurrently
.
Basic Example
package main
import (
"fmt"
"time"
)
func print_nice(name string) {
for i := 0; i < 10; i++ {
fmt.Printf("%d from %s\n", i, name)
time.Sleep(1 * time.Second)
}
}
func main() {
go print_nice("Corinthians")
go print_nice("Knicks")
go func() {
for i := 0; i < 4; i++ {
fmt.Println("Just an annoying print")
// time.Sleep(1 * time.Second)
}
}()
}
Just an annoying print
0 from Corinthians
0 from Knicks
1 from Knicks
Just an annoying print
1 from Corinthians
2 from Corinthians
2 from Knicks
Just an annoying print
3 from Corinthians
Just an annoying print
3 from Knicks
4 from Knicks
4 from Corinthians
5 from Knicks
5 from Corinthians
6 from Knicks
6 from Corinthians
7 from Knicks
7 from Corinthians
8 from Knicks
8 from Corinthians
9 from Knicks
9 from Corinthians
sync.WaitGroup
A sync.WaitGroup
in Go is used to wait for a collection of goroutines to finish
. It's a way to ensure that all goroutines have completed their execution before the program continues. This is particularly useful when you have multiple goroutines performing tasks concurrently and you need to wait for all of them to finish before proceeding
.
Before starting a goroutine, you call the Add
method on the WaitGroup to increment the counter
. This tells the WaitGroup
to wait for one more goroutine to finish
. Inside the goroutine, when the work is done, you call the Done
method on the WaitGroup to decrement the counter
. Finally, you call the Wait
method on the WaitGroup to block the current goroutine until the counter is zero
.
Example
package main
import (
"fmt"
"sync"
"time"
)
func print_nice(name string, wg *sync.WaitGroup) {
for i := 0; i < 10; i++ {
fmt.Printf("%d from %s\n", i, name)
time.Sleep(1 * time.Second)
wg.Done()
}
}
func main() {
waitGroup := sync.WaitGroup{}
waitGroup.Add(24)
go print_nice("Corinthians", &waitGroup)
go print_nice("Knicks", &waitGroup)
go func() {
for i := 0; i < 4; i++ {
fmt.Println("Just an annoying print")
time.Sleep(1 * time.Second)
waitGroup.Done()
}
}()
waitGroup.Wait()
}
Just an annoying print
0 from Corinthians
0 from Knicks
1 from Knicks
Just an annoying print
1 from Corinthians
2 from Corinthians
2 from Knicks
Just an annoying print
3 from Corinthians
Just an annoying print
3 from Knicks
4 from Knicks
4 from Corinthians
5 from Knicks
5 from Corinthians
6 from Knicks
6 from Corinthians
7 from Knicks
7 from Corinthians
8 from Knicks
8 from Corinthians
9 from Knicks
9 from Corinthians
If you need to set a timeout
for the WaitGroup, you can use a select
statement with a time.After
function.
With Timeout
package main
import (
"fmt"
"log"
"sync"
"time"
)
func print_nice(name string, wg *sync.WaitGroup) {
for i := 0; i < 10; i++ {
fmt.Printf("%d from %s\n", i, name)
time.Sleep(1 * time.Second)
defer wg.Done()
}
}
func main() {
waitGroup := sync.WaitGroup{}
waitGroup.Add(14)
go print_nice("Corinthians", &waitGroup)
go func() {
for i := 0; i < 4; i++ {
fmt.Println("Just an annoying print")
time.Sleep(1 * time.Second)
waitGroup.Done()
}
}()
done := make(chan bool)
go func() {
waitGroup.Wait()
// Close the Channel
close(done)
}()
select {
case <-done:
log.Println("All done")
case <-time.After(9 * time.Second):
log.Println("Hit timeout")
}
}
Just an annoying print
0 from Corinthians
1 from Corinthians
Just an annoying print
2 from Corinthians
Just an annoying print
3 from Corinthians
Just an annoying print
4 from Corinthians
5 from Corinthians
6 from Corinthians
7 from Corinthians
8 from Corinthians
2023/12/18 18:43:22 Hit timeout
When to Use Goroutines
Goroutines are useful when one task can be split into different segments to perform better
. Any work that can utilize a multi-core CPU should be well optimized using goroutines. Running background operations
in a program might also be a use case for a goroutine.
Real-Life Use Cases
Some real-life use cases of goroutines include reading a huge file and processing it
for exception or error messages, and posting multiple API calls in different threads when they are not dependent on each other
.
Goroutines x Normal Threads/Process
Goroutines and threads are both used for concurrent execution of code
, but they have significant differences in terms of scheduling, communication, execution speed, infrastructure dependency, stack management, latency during program execution, resource control, and local storage management
.
Goroutines are extremely cheap when compared to threads
. They are only a few kilobytes in stack size
and the stack can grow and shrink according to the needs of the application. There might be only one thread in a program with thousands of Goroutines
. If any Goroutine in that thread blocks, then another OS thread is created and the remaining Goroutines are moved to the new OS thread.
Scheduling Management
Goroutines are managed by the Go runtime
, which uses a technique known as m:n scheduling, where m goroutines are executed using n operating system threads using multiplexing
. This means that the Go scheduler is not invoked periodically by a hardware timer
, but implicitly by certain Go language constructs. Because it doesn’t need a switch to kernel context, rescheduling a goroutine is much cheaper than rescheduling a thread
.
Example
For example, when a goroutine calls time.Sleep
or blocks
in a channel or mutex operation, the scheduler puts it to sleep and runs another goroutine until it is time to wake the first one up.
On the other hand, threads are managed by the operating system
. Every few milliseconds, a hardware timer interrupts the processor, which causes a kernel function called the scheduler to be invoked. This function suspends the currently executing thread and saves its registers in memory
, looks over the list of threads and decides which one should run next, restores that thread’s registers from memory, then resumes the execution of that thread. Because OS threads are scheduled by the kernel, passing control from one thread to another requires a full context switch, which is a slow operation
.
Communication Medium
Goroutines enhance communication through the use of a channel and sync package
which provides a wait group function. Threads do not have a clear communication medium
. In multiple threads executing, communication is made through memory location
.
Infrastructure Dependency
Goroutines are not hardware dependent
, meaning they can be executed independently of any infrastructure. Threads, are ardware dependent
.
Stack Size
Goroutines are executed in a stack of 2kb (kilobytes)
, which grows gradually
and is destroyed once execution is done/completed
. Threads also execute in a stack, but they require at least a minimum of 1 megabyte
to execute, and stack size is fixed
. Thus, stack management is easier with goroutines compared to threads.
Latency During Program Execution
Goroutines communicate with each other through
channels, thus low latency is experienced from one channel to another. In threads, since there is no communication medium between one thread to another, communication takes place with high latency.