Condition variables let you block until a condition is met. For example, let's say that we're writing a little TCP server that can have up to MAX_CLIENTS connected. We might start with:
import ( "net" "sync" ) type Server struct { sync.Mutex clients int } func (s *Server) Listen(address string) { l, err := net.Listen("tcp", address) if err != nil { panic(err) } defer l.Close() for { conn, err := l.Accept() if err != nil { //to do log this continue } s.Lock() s.clients++ s.Unlock() go s.handleClient(conn) } } func (s *Server) handleClient(conn net.Conn) { defer s.disconnected() for { // ... } } func (s *Server) disconnected() { s.Lock() s.clients-- s.Unlock() }
One way to limit the total number of clients would be to check the value of s.clients
within a loop:
for { s.Lock() for s.clients == MAX_CLIENTS { s.Unlock() time.Sleep(time.Second) s.Lock() } s.Unlock() conn, err := l.Accept() ... }
A more elegant solution is to use a condition variable. Condition variables provide a simple mechanism which our goroutines can use to signal a change to s.clients
. First, we define the condition variable:
import ( "net" "sync" "sync/atomic" ) type Server struct { clients uint64 cond *sync.Cond }
Condition variable are made up of their own mutex. To iniate one, we'd do:
s := &Server{ cond: &sync.Cond{L: &sync.Mutex{}}, }
Next, instead of the above for
spin, we can Wait
for a signal:
for { s.cond.L.Lock() for s.listeners == MAX_CLIENTS { s.cond.Wait() } s.cond.L.Unlock() conn, err := l.Accept() ... }
And we change our disconnected
method:
func (s *Server) disconnected() { s.cond.L.Lock() s.clients-- s.cond.L.Unlock() s.cond.Signal() }
There are a couple interesting things in the above code. First of all, notice the locking and unlocking around the call to Wait
. It might seem like we're locking for a very long time. But Wait
unlocks L
on entry and relocks L
on exit. This results in much cleaner code -- you lock and unlock normally, without being locked while you wait.
Also, notice that we're still checking our condition inside of a loop. This is because the state of s.clients
could be changed by a different goroutine between the time that the signal is sent and our code exiting Wait
. (In this specific example, when the blocked goroutine is also the only one that can increment s.clients
, the for loop is unecessary. But I wanted to show the for loop example anyways because it's more complete and more common).