DEV Community

Shrijith Venkatramana
Shrijith Venkatramana

Posted on • Edited on

Concurrency Done Right: Go’s Condition Variables

Hello, I'm Shrijith. I'm working on git-lrc: a Git hook for Checking AI generated code.

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...r 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)
}
Enter fullscreen mode Exit fullscreen mode

My Machine Reports

Busy-wait iterations: 8,168,421,879
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

My Machine Reports

Sleep-based iterations: 195
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

My Machine Reports...

Condition variable wake-ups: 1
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Sample Output

Worker 1 Processing 42
Worker 2 Processing 43
Worker 3 Processing 44
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

git-lrc
*AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.*

Any feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.

⭐ Star it on GitHub:

GitHub logo HexmosTech / git-lrc

Free, Unlimited AI Code Reviews That Run on Commit

git-lrc logo

git-lrc

Free, Unlimited AI Code Reviews That Run on Commit


git-lrc - Free, unlimited AI code reviews that run on commit | Product Hunt

AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.

See It In Action

See git-lrc catch serious security issues such as leaked credentials, expensive cloud operations, and sensitive material in log statements

git-lrc-intro-60s.mp4

Why

  • 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won't notice until production.
  • 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong.
  • 🔁 Build a habit, ship better code. Regular review → fewer bugs → more robust code → better results in your team.
  • 🔗 Why git? Git is universal. Every editor, every IDE, every AI…




Top comments (0)