Free tools Windows power users keep installed
One-click scans. No signup required.
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
sync.Cond is Go’s condition-variable primitive. It lets goroutines sleep until shared state may have changed, then wake one another with Signal or Broadcast. The essential pattern is:
mu.Lock()
for !condition {
cond.Wait()
}
useSharedState()
mu.Unlock()
The condition itself is ordinary application state—such as ready == true or len(queue) > 0. A mutex protects that state; sync.Cond coordinates goroutines waiting for it.
What problem does sync.Cond solve?
Concurrency has two separate problems:
- Mutual exclusion: only one goroutine should access shared state at a time. Use
sync.Mutexorsync.RWMutex. - Waiting for state: a goroutine should pause until a queue has an item, initialization is complete, or capacity is available. Use a condition variable or another coordination mechanism.
A mutex protects a condition, but it does not provide an efficient way to sleep until that condition changes. This busy-waiting loop wastes CPU:
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →for !ready {
// Repeated polling
}
sync.Cond allows the goroutine to sleep while another goroutine changes the shared state.
#1 Best Overall
What is a condition or predicate?
A Cond does not store or understand the condition. Your program owns the predicate and the data it examines. Examples include:
ready == true
len(queue) > 0
len(queue) < capacity
activeWorkers == 0
state == "closed"
The predicate’s state must be read and changed consistently while holding the associated lock. A notification means that the predicate may now be true; it does not guarantee that it is true when the awakened goroutine gets the lock.
Creating a condition variable
Create a condition variable with sync.NewCond and a locker, normally a pointer to a mutex:
mu := &sync.Mutex{}
cond := sync.NewCond(mu)
The locker must implement:
type Locker interface {
Lock()
Unlock()
}
A *sync.RWMutex can also implement the interface, but condition-variable designs involving read locks are easier to misuse. For most code, use *sync.Mutex unless there is a clear reason not to.
Unlike a mutex, a Cond needs an associated locker supplied through NewCond. Prefer a constructor when a type owns one:
type Gate struct {
mu sync.Mutex
cond *sync.Cond
ready bool
}
func NewGate() *Gate {
g := &Gate{}
g.cond = sync.NewCond(&g.mu)
return g
}
Do not copy a Cond after it has been used. Prefer pointers to objects containing synchronization primitives.
See the official sync package documentation and sync.Cond source documentation for the API contract.
How Wait works
A caller must hold cond.L before calling Wait. Wait then:
- Registers the goroutine as a waiter.
- Atomically unlocks the associated locker.
- Suspends the goroutine.
- Resumes after
SignalorBroadcast. - Reacquires the locker before returning.
That atomic unlock-and-wait step prevents a notification from being lost between checking the predicate and going to sleep.
mu.Lock()
for !condition() {
cond.Wait()
}
// The lock is held again here.
useSharedState()
mu.Unlock()
The lock is not held while the goroutine sleeps, but it is held again when Wait returns.
Why Wait must be inside a for loop
Use this:
mu.Lock()
for len(queue) == 0 {
cond.Wait()
}
item := queue[0]
queue = queue[1:]
mu.Unlock()
Do not use a single if:
mu.Lock()
if len(queue) == 0 {
cond.Wait()
}
item := queue[0] // The queue may be empty again.
mu.Unlock()
Suppose several consumers are waiting and a producer adds one item, then calls Broadcast. All consumers wake, but only one can acquire the mutex first and remove the item. The others must wait for the lock, then recheck the predicate and go back to sleep.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Go documents that Wait does not return unless awakened by Signal or Broadcast. Nevertheless, a loop is mandatory because waking means “the state may have changed,” not “the condition is reserved for this goroutine.”
Signal versus Broadcast
| Method | Effect | Typical use |
|---|---|---|
Signal |
Wakes at most one waiter. | One newly added queue item can satisfy one consumer. |
Broadcast |
Wakes all current waiters. | Shutdown, readiness transitions, or state changes that may help many waiters. |
Signal does not promise FIFO order, fairness, or scheduling priority. Code must not depend on which waiter runs first.
The API permits calling either method with or without holding cond.L. In practice, holding the lock while changing the predicate and notifying is usually easier to reason about:
mu.Lock()
ready = true
cond.Broadcast()
mu.Unlock()
The lock is still required when safely reading or changing the predicate, even though it is not formally required just to call Signal or Broadcast.
Example: wait until initialization is ready
package main
import (
"fmt"
"sync"
)
type Starter struct {
mu sync.Mutex
cond *sync.Cond
ready bool
}
func NewStarter() *Starter {
s := &Starter{}
s.cond = sync.NewCond(&s.mu)
return s
}
func (s *Starter) WaitUntilReady() {
s.mu.Lock()
defer s.mu.Unlock()
for !s.ready {
s.cond.Wait()
}
}
func (s *Starter) SetReady() {
s.mu.Lock()
defer s.mu.Unlock()
s.ready = true
s.cond.Broadcast()
}
func main() {
starter := NewStarter()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
starter.WaitUntilReady()
fmt.Println("worker: starting")
}()
// Initialization would happen here in a real program.
starter.SetReady()
wg.Wait()
}
The ready field is protected by mu. The waiter releases the mutex while sleeping, and SetReady updates the state and wakes all current waiters. A future waiter simply observes ready == true and does not wait.
Notifications are not queued events
A condition variable is not a message queue. This is not a durable event:
cond.Signal()
If nobody is waiting at that moment, there may be nobody to wake. Store durable information in shared state instead:
mu.Lock()
ready = true
cond.Broadcast()
mu.Unlock()
A later waiter sees ready == true and proceeds. If the program needs to retain messages or events, a channel or explicit queue is usually the better abstraction.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallExample: a bounded producer–consumer queue
A bounded queue has two useful predicates:
notEmpty: consumers can proceed whenlen(items) > 0.notFull: producers can proceed whenlen(items) < capacity.
package queue
import (
"errors"
"sync"
)
var ErrClosed = errors.New("queue is closed")
type Queue[T any] struct {
mu sync.Mutex
notEmpty *sync.Cond
notFull *sync.Cond
items []T
cap int
closed bool
}
func NewQueue[T any](capacity int) *Queue[T] {
if capacity <= 0 {
panic("capacity must be positive")
}
q := &Queue[T]{cap: capacity}
q.notEmpty = sync.NewCond(&q.mu)
q.notFull = sync.NewCond(&q.mu)
return q
}
func (q *Queue[T]) Put(item T) error {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == q.cap && !q.closed {
q.notFull.Wait()
}
if q.closed {
return ErrClosed
}
q.items = append(q.items, item)
q.notEmpty.Signal()
return nil
}
func (q *Queue[T]) Get() (T, error) {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 && !q.closed {
q.notEmpty.Wait()
}
if len(q.items) == 0 && q.closed {
var zero T
return zero, ErrClosed
}
item := q.items[0]
q.items[0] = *new(T)
q.items = q.items[1:]
q.notFull.Signal()
return item, nil
}
func (q *Queue[T]) Close() {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed {
return
}
q.closed = true
q.notEmpty.Broadcast()
q.notFull.Broadcast()
}
Adding one item signals one consumer. Removing one item signals one producer. Closing broadcasts to both groups because blocked producers and consumers must be given a chance to observe closed and exit.
This example drains already-buffered items after closure: consumers receive those items before returning ErrClosed. A production queue may instead choose different policies, such as cancellation with a context, rejecting all operations immediately, or returning a separate shutdown error.
Rank #4
Common mistakes
Calling Wait without the lock
cond.Wait() // Incorrect: cond.L is not held.
The caller must hold the associated locker when calling Wait.
Using if instead of for
Always recheck the predicate after waking. Another goroutine may have consumed the resource first.
Reading the predicate outside the lock
if ready { ... } // A race if another goroutine writes ready.
Protect every relevant read and write with the same mutex, or use a properly designed atomic operation when a mutex is not appropriate.
Signaling without changing durable state
A notification should normally accompany a state transition. Otherwise, a waiter may wake, find the predicate false, and sleep again without progress.
Forgetting shutdown
This waiter can remain blocked forever if the queue closes while empty:
for len(queue) == 0 {
cond.Wait()
}
Include shutdown in the predicate:
for len(queue) == 0 && !closed {
cond.Wait()
}
Holding the mutex during slow work
Claim or copy the needed state, then unlock before expensive or blocking work. Otherwise, other goroutines may be unable to change the predicate or wake waiters.
Expecting fairness
Signal wakes one waiter but does not guarantee which waiter runs. Do not build correctness around FIFO behavior.
Best Value
Copying a used Cond
Do not pass a used Cond by value or copy a struct containing one after coordination has begun. Use pointers and constructors.
Memory visibility and happens-before
The producer should update shared state under the mutex, and the consumer should inspect it under the same mutex. This gives the program a synchronized lock boundary instead of unsafely sharing ordinary variables between goroutines.
Go’s sync.Cond documentation specifies that a Signal or Broadcast synchronizes before the Wait call it unblocks. The condition variable provides waiting and notification; it does not replace the mutex that protects the state. See the Go memory model for the broader rules.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Cancellation and timeouts
sync.Cond has no built-in timeout or context-aware Wait. Cancellation must be represented in the predicate and followed by a notification:
for !ready && !cancelled {
cond.Wait()
}
The cancellation path must set cancelled under the mutex and call Broadcast so blocked goroutines can observe it. If deadlines, cancellation, or select are central to the design, channels and context.Context are often easier to compose.
sync.Cond versus channels and other tools
| Need | Good default |
|---|---|
| Transfer work or results | Channel |
| Wait for permanent readiness | Closed channel or sync.Once, depending on the lifecycle |
| Manage a shared bounded queue | sync.Cond or a channel-based queue |
| Wake all waiters during shutdown | Broadcast or close a channel |
Timeouts, cancellation, or select |
Channel plus context.Context |
| One numeric flag or counter | sync/atomic, if the design truly needs atomic access |
| Limit concurrent work | Buffered channel semaphore or a suitable semaphore abstraction |
Use sync.Cond when several goroutines wait on persistent shared state protected by a mutex, especially when there are multiple predicates such as “not empty” and “not full.” Use channels when the central operation is communication or value transfer. Go’s documentation explicitly notes that channels are preferable for many simple use cases, but they are not a universal replacement for condition variables.
Do not assume that sync.Cond is always faster than channels. The right choice depends on semantics, ownership, cancellation requirements, clarity, and the workload.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesTesting a condition-variable design
Create a temporary module if you want to experiment:
mkdir cond-demo
cd cond-demo
go mod init example.com/cond-demo
Run tests with the race detector:
go test
go test -race
go run -race .
Tests should cover:
- A worker remains blocked while readiness is false.
- Setting readiness allows the worker to continue.
- Several waiters all continue after
Broadcast. - A consumer cannot remove an item from an empty queue.
Closewakes blocked producers and consumers.- Repeated producer–consumer runs do not deadlock.
Avoid asserting exact goroutine scheduling or assuming that Signal selects a particular waiter. The race detector finds races only on executed paths, so meaningful tests must exercise waiting, signaling, broadcasting, and shutdown. More details are available in Go’s race detector documentation.
Quick Recap
A practical checklist
- Is the predicate ordinary shared state protected by the associated lock?
- Is every
Waitcall inside aforloop? - Is the lock held when observing or changing the predicate?
- Does each relevant state transition notify the right waiters?
- Does the predicate include shutdown or cancellation?
- Is
Signalused for one available unit andBroadcastfor global transitions? - Does the code avoid relying on fairness or FIFO ordering?
- Is the
Condnever copied after first use? - Have tests been run with
go test -race?
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

