Mastering Go Synchronization Primitives: Mutex, RWMutex, Cond, Semaphore & WaitGroup
This article explains the purpose, types, best practices, and common use cases of synchronization primitives—including mutexes, condition variables, semaphores, and wait groups—in Go, and provides concrete code examples to illustrate safe concurrent programming.
Synchronization primitives are mechanisms that control the execution order of processes or threads, ensuring consistent access to shared resources and preventing data races.
Two main categories of synchronization primitives
Mutex (Mutual Exclusion Lock) : protects shared resources by allowing only one thread to access them at a time; it has locked and unlocked states.
Condition Variable (Cond) : used under a mutex to wait for a specific condition; threads wait with Wait and are awakened with Signal or Broadcast.
When using these primitives, keep in mind:
Avoid over‑synchronization : excessive locking hurts performance; use primitives only when necessary.
Use them correctly : improper use can cause deadlocks.
Prefer higher‑level mechanisms : many languages, including Go, offer channels or other abstractions that are often safer than low‑level primitives.
Typical scenarios for synchronization primitives include:
Protecting shared resources : ensure only one thread modifies a resource at a time.
Coordinating thread execution order : enforce a specific sequence of operations across threads.
Improving performance : e.g., using a mutex to avoid contention and increase cache‑hit rates.
Go provides the following synchronization primitives:
Mutex : standard mutual‑exclusion lock for goroutines.
RWMutex : allows multiple readers or a single writer, improving read‑heavy concurrency.
Cond : condition variable that works with a mutex to wait for conditions.
Semaphore : controls access count to a resource via Acquire and Release.
WaitGroup : waits for a collection of goroutines to finish using a counter.
Usage examples
// Mutex protecting a shared counter
var m sync.Mutex
var count int
func increment() {
m.Lock()
count++
m.Unlock()
}
func decrement() {
m.Lock()
count--
m.Unlock()
}
// Condition variable waiting for a flag
var c sync.Cond
var flag bool
func wait() {
c.L.Lock()
for !flag {
c.Wait()
}
c.L.Unlock()
}
func signal() {
c.L.Lock()
flag = true
c.Signal()
c.L.Unlock()
}
// Semaphore controlling access
var s sync.Semaphore
func acquire() {
s.Acquire()
// use shared resource
s.Release()
}
// WaitGroup waiting for goroutines
var wg sync.WaitGroup
func worker() {
// perform task
wg.Done()
}
func main() {
wg.Add(10)
for i := 0; i < 10; i++ {
go worker()
}
wg.Wait()
}When applying Go's synchronization primitives, remember to avoid over‑synchronization, use them correctly to prevent deadlocks, and prefer higher‑level constructs like channels whenever they meet the requirements.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Ops Development & AI Practice
DevSecOps engineer sharing experiences and insights on AI, Web3, and Claude code development. Aims to help solve technical challenges, improve development efficiency, and grow through community interaction. Feel free to comment and discuss.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
