Understanding Go Scheduler
Explore how the Go scheduler works and how it manages goroutines efficiently.
Go's scheduler is a key component that enables efficient execution and management of goroutines. Unlike traditional OS-level threads, goroutines are managed by the Go runtime, which allows for more lightweight concurrency.
Basics of Go Scheduler
Go’s runtime uses an M:N scheduling model (M goroutines on N OS threads) GOMAXPROCS controlling the number of threads that run Go code in parallel.
Example: Setting GOMAXPROCS
package main
import (
"fmt"
"runtime"
"sync"
)
func printGreetings(wg *sync.WaitGroup, id int) {
defer wg.Done()
fmt.Printf("Hello from goroutine %d\n", id)
}
func main() {
runtime.GOMAXPROCS(2) // Set the maximum number of OS threads to 2
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go printGreetings(&wg, i)
}
wg.Wait()
}
Cooperative Multitasking
Go uses cooperative multitasking for goroutines, where goroutines yield control to the scheduler during specific operations, such as waiting on I/O or using runtime.Gosched()
.
Example: Cooperative Scheduling with runtime.Gosched()
package main
import (
"fmt"
"runtime"
"sync"
)
func task(wg *sync.WaitGroup, id int) {
defer wg.Done()
for i := 0; i < 3; i++ {
fmt.Printf("Goroutine %d executed\n", id)
runtime.Gosched() // Yield to other goroutines
}
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go task(&wg, i)
}
wg.Wait()
}
Work Stealing
The Go scheduler uses a work-stealing algorithm to balance load across multiple processors. If a processor has no work to do, it can steal work from another processor.
Best Practices
- Set
GOMAXPROCS
according to your application's concurrency requirements and the number of logical CPU cores available (runtime.NumCPU()
). - Use synchronization primitives like channels and
sync.WaitGroup
to manage goroutine lifecycles efficiently. - Avoid relying on goroutine scheduling order for program correctness—it can vary between runs.
Common Pitfalls
- Overloading the scheduler with excessive goroutines (e.g. thousands, tens of thousands) without synchronization, causing resource contention.
- Assuming that goroutines will always run in the order they are created.
- Forgetting to decrease wait groups or failing to synchronize goroutines leading to deadlocks.
Performance Tips
- Keep goroutine creation to a necessary minimum, especially in tight loops, to avoid overwhelming the scheduler.
- Profile your application using tools like
go tool pprof
to understand and optimize goroutine performance. - Use
runtime.Gosched()
judiciously to yield control only when necessary—it can introduce unnecessary context switching if overused.