Semaphores, Rendezvous, Barriers
Semaphores, Rendezvous, Barriers
Overview
Limit concurrency to ≤ N (semaphore), meet at a sync point (rendezvous), or wait for everyone before next phase (barrier).
Mutex is N=1
type External struct{ lock sync.Mutex }
func (e *External) Call() {
e.lock.Lock()
defer e.lock.Unlock()
// only one call at a time
}When the dependency allows 4 parallel calls, a mutex is too strict.
Semaphore model
slots: [free free free free] capacity 4
Acquire: take a free slot or block
Release: free a slot; wake one waiter
semaphore capacity N: [free free …]
Acquire → take slot or block
Release → free slot, wake waiter
rendezvous: each waits until the other arrives
Channel implementation
Stdlib has no Semaphore type; a buffered channel is the idiomatic simple form (golang.org/x/sync/semaphore for weighted cases).
type Semaphore chan struct{}
func NewSemaphore(n int) Semaphore {
return make(chan struct{}, n)
}
func (s Semaphore) Acquire() { s <- struct{}{} }
func (s Semaphore) Release() { <-s }
func (s Semaphore) TryAcquire() bool {
select {
case s <- struct{}{}:
return true
default:
return false
}
}Where to acquire
| Pattern | Effect |
|---|---|
| Acquire inside worker | May spawn huge number of blocked Gs |
Acquire before go |
Bounds goroutine count as well as concurrency |
for range nCalls {
sem.Acquire()
wg.Go(func() {
defer sem.Release()
ex.Call()
})
}Rendezvous
Two parties wait for each other before continuing:
G1 reaches point ──wait──┐
├── both proceed
G2 reaches point ──wait──┘
ready1 := make(chan struct{})
ready2 := make(chan struct{})
// G1:
close(ready1)
<-ready2
// G2:
close(ready2)
<-ready1sequence (top → bottom):
actors: G1, G2
G1 --> G1 : close ready1
G1 --> G2 : wait ready2
G2 --> G2 : close ready2
G2 --> G1 : wait ready1
note: both unblocked
Caution: concurrent fmt itself synchronizes and can hide races when debugging.
Barrier (N parties)
Generalize rendezvous to N workers: each Add(1) to a cycle, last arriver releases all. Patterns:
sync.WaitGroupfor one-shot phase joins- Two WaitGroups or condition variables for reusable barriers
- Channel of capacity 0 with careful N-handshake (easy to get wrong)
Teaching pattern for one phase:
var start, done sync.WaitGroup
start.Add(n)
done.Add(n)
for i := 0; i < n; i++ {
go func() {
start.Done()
start.Wait() // all started
// phase work
done.Done()
}()
}
done.Wait()Runnable example
go mod init example && go run .package main
import (
"fmt"
"sync"
"time"
)
func main() {
sem := make(chan struct{}, 4)
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 12; i++ {
sem <- struct{}{}
wg.Go(func() {
defer func() { <-sem }()
time.Sleep(10 * time.Millisecond)
fmt.Print(".")
})
}
wg.Wait()
fmt.Printf("\n%d ms\n", time.Since(start).Milliseconds())
}Expected: ~30ms (12/4 × 10ms), twelve dots.
Try next: Implement rendezvous so two printers always print “sync” before either prints “after”.