Concurrency Done Right: Go’s Condition Variables
Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy. In Go concurrency, goroutines often need to wait for work, but doing so efficiently can be a challenge—constant checks waste CPU time, while delays slow things down. Condition variables, via sync.Cond, offer a solution by minimizing resource use and improving response times. In this post, we’ll explore how they address this issue and why understanding them can make you a more effective Go engineer. 1. The Problem: Wasting CPU Cycles Picture a worker goroutine tasked with processing items from a queue. A simple but inefficient solution is to have it constantly check the queue in a loop, burning CPU cycles while waiting for work. Case 1: Busy-Waiting Worker (Relentless Looping) package main import ( "fmt" "time" ) var queue []int var iterations int func worker() { for { iterations++ // Track each check if len(queue) == 0 { continue } break } } func main() { go worker() time.Sleep(2 * time.Second) // Let it spin fmt.Println("Busy-wait iterations:", iterations) } My Machine Reports Busy-wait iterations: 8,168,421,879 In just 2 seconds, this worker churned through over 8 billion iterations—all for nothing. That’s a staggering amount of CPU time wasted on empty checks. 2. Adding Sleep: Less Waste, Slower Response To curb the CPU hogging, a common tweak is to pause between checks using time.Sleep. package main import ( "fmt" "time" ) var sleepIterations int func worker() { queue := []int{} for { sleepIterations++ if len(queue) == 0 { time.Sleep(10 * time.Millisecond) continue } break } } func main() { go worker() time.Sleep(2 * time.Second) fmt.Println("Sleep-based iterations:", sleepIterations) } My Machine Reports Sleep-based iterations: 195 Now we’re down to ~200 checks instead of 8 billion—a huge improvement. But there’s a catch: the worker still wakes up periodically to check an empty queue, delaying its response when real work arrives. 3. Enter Condition Variables: Smart Waiting A condition variable offers a better way. It lets the worker sleep efficiently until explicitly signaled, slashing CPU waste and improving responsiveness. package main import ( "sync" "fmt" "time" ) var cond = sync.NewCond(&sync.Mutex{}) var condIterations int func worker() { queue := []int{} cond.L.Lock() for len(queue) == 0 { condIterations++ cond.Wait() // Sleep until signaled } cond.L.Unlock() } func main() { go worker() time.Sleep(2 * time.Second) fmt.Println("Condition variable wake-ups:", condIterations) } Sample Output Condition variable wake-ups: 1 Here, the worker sleeps completely, waking up just once when there’s work to do. No CPU cycles are squandered on pointless checks. 4. Scaling Up: Condition Variables with Multiple Goroutines Now let’s see condition variables in action with multiple workers sharing a queue. package main import ( "fmt" "sync" "time" ) var queue []int var cond = sync.NewCond(&sync.Mutex{}) func worker(id int) { for { cond.L.Lock() for len(queue) == 0 { cond.Wait() } // Process one item if len(queue) > 0 { item := queue[0] queue = queue[1:] fmt.Println("Worker", id, "Processing", item) // Signal after unlocking to avoid blocking others defer cond.Signal() } cond.L.Unlock() // Brief pause to let other workers run time.Sleep(10 * time.Millisecond) } } func main() { for i := 1; i

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy.
In Go concurrency, goroutines often need to wait for work, but doing so efficiently can be a challenge—constant checks waste CPU time, while delays slow things down.
Condition variables, via sync.Cond, offer a solution by minimizing resource use and improving response times.
In this post, we’ll explore how they address this issue and why understanding them can make you a more effective Go engineer.
1. The Problem: Wasting CPU Cycles
Picture a worker goroutine tasked with processing items from a queue. A simple but inefficient solution is to have it constantly check the queue in a loop, burning CPU cycles while waiting for work.
Case 1: Busy-Waiting Worker (Relentless Looping)
package main
import (
"fmt"
"time"
)
var queue []int
var iterations int
func worker() {
for {
iterations++ // Track each check
if len(queue) == 0 {
continue
}
break
}
}
func main() {
go worker()
time.Sleep(2 * time.Second) // Let it spin
fmt.Println("Busy-wait iterations:", iterations)
}
My Machine Reports
Busy-wait iterations: 8,168,421,879
In just 2 seconds, this worker churned through over 8 billion iterations—all for nothing. That’s a staggering amount of CPU time wasted on empty checks.
2. Adding Sleep: Less Waste, Slower Response
To curb the CPU hogging, a common tweak is to pause between checks using time.Sleep
.
package main
import (
"fmt"
"time"
)
var sleepIterations int
func worker() {
queue := []int{}
for {
sleepIterations++
if len(queue) == 0 {
time.Sleep(10 * time.Millisecond)
continue
}
break
}
}
func main() {
go worker()
time.Sleep(2 * time.Second)
fmt.Println("Sleep-based iterations:", sleepIterations)
}
My Machine Reports
Sleep-based iterations: 195
Now we’re down to ~200 checks instead of 8 billion—a huge improvement. But there’s a catch: the worker still wakes up periodically to check an empty queue, delaying its response when real work arrives.
3. Enter Condition Variables: Smart Waiting
A condition variable offers a better way. It lets the worker sleep efficiently until explicitly signaled, slashing CPU waste and improving responsiveness.
package main
import (
"sync"
"fmt"
"time"
)
var cond = sync.NewCond(&sync.Mutex{})
var condIterations int
func worker() {
queue := []int{}
cond.L.Lock()
for len(queue) == 0 {
condIterations++
cond.Wait() // Sleep until signaled
}
cond.L.Unlock()
}
func main() {
go worker()
time.Sleep(2 * time.Second)
fmt.Println("Condition variable wake-ups:", condIterations)
}
Sample Output
Condition variable wake-ups: 1
Here, the worker sleeps completely, waking up just once when there’s work to do. No CPU cycles are squandered on pointless checks.
4. Scaling Up: Condition Variables with Multiple Goroutines
Now let’s see condition variables in action with multiple workers sharing a queue.
package main
import (
"fmt"
"sync"
"time"
)
var queue []int
var cond = sync.NewCond(&sync.Mutex{})
func worker(id int) {
for {
cond.L.Lock()
for len(queue) == 0 {
cond.Wait()
}
// Process one item
if len(queue) > 0 {
item := queue[0]
queue = queue[1:]
fmt.Println("Worker", id, "Processing", item)
// Signal after unlocking to avoid blocking others
defer cond.Signal()
}
cond.L.Unlock()
// Brief pause to let other workers run
time.Sleep(10 * time.Millisecond)
}
}
func main() {
for i := 1; i <= 3; i++ {
go worker(i)
}
time.Sleep(1 * time.Second)
cond.L.Lock()
queue = append(queue, 42, 43, 44) // Add items
cond.Broadcast() // Wake all workers
cond.L.Unlock()
time.Sleep(3 * time.Second)
}
Sample Output
Worker 1 Processing 42
Worker 2 Processing 43
Worker 3 Processing 44
Go’s sync.Cond
enables clean coordination. Workers wait patiently:
cond.L.Lock() // Protect the queue
for len(queue) == 0 {
cond.Wait() // Release lock and sleep
}
When cond.Wait()
runs, the goroutine:
- Releases the mutex
- Suspends itself
- Reclaims the lock upon waking
A producer adds work and signals:
cond.L.Lock()
queue = append(queue, 42, 43, 44)
cond.Broadcast() // Wake all waiting workers
cond.L.Unlock()
Each worker processes an item and passes the baton:
item := queue[0]
queue = queue[1:]
fmt.Println("Worker", id, "Processing", item)
cond.Signal() // Notify the next worker
This creates a smooth handoff, ensuring work continues as long as items remain.
5. Why Condition Variables Matter
Approach | CPU Checks | Behavior |
---|---|---|
Busy-Waiting | 5 million+ | Relentless polling |
Sleep Strategy | 200 | Periodic delays |
Condition Var | 1 | Wakes only on need |
Condition variables shine by eliminating waste and waking workers precisely when there’s something to do. For efficient, responsive waiting in Go, sync.Cond
is your go-to tool.
Happy coding!