Condition Variables
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).