home

Condition Variables

Nov 03, 2014

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