The second problem in the “Less classical problems” chapter of the “The Little Book of Semaphores” is called “the barbershop problem”. It as originally proposed by Dijkstra as “the sleeping barber” program. The book presents the problem as follows:
A barbershop consists of a waiting room with n chairs, and the barber room containing the barber chair. If there are no customers to be served, the barber goes to sleep. If a customer enters the barbershop and all chairs are occupied, then the customer leaves the shop. If the barber is busy, but chairs are available, then the customer sits in one of the free chairs. If the barber is asleep, the customer wakes up the barber. Write a program to coordinate the barber and the customers.
At first reading, this sounds like a job for a buffered channel, because
it would be able to hold waiting n
customers until the barber services
them. That approach doesn’t work because when the n+1 customer shows up
before the barber was put the first one on the chair, sending on that
channel will block, instead of just fail, meaning it’s not possible to
implement the “leaves the shop” behavior.
A better approach is to implement a Barbershop
that provides a channel
where clients can request service. If the client can be serviced, it’s
put on the waitlist. Meanwhile the barber is either waiting for the next
customer or cutting the hair of the current one. A possible
implementation looks like this:
type Barbershop struct {
WaitRoomSize int
WaitingCh chan bool
BarbershopCh chan Customer
}
The BarbershopCh
is where customers enter the barbershop and the
WaitingCh
is where customers are told if they on the wait list or not.
The Barbershop
runs the following loop:
for {
c := <-b.BarbershopCh:
switch {
case len(barberCh) == b.WaitRoomSize:
// tell the customer that we are full
b.WaitingCh <- false
default:
// add customer to wait list
barberCh <- c
log.Printf("Customer %s waiting", c.Name)
// tell the customer that they are waiting
b.WaitingCh <- true
}
}
There’s an “internal” channel between the wait room and the barber
(barberCh
). This channel is buffered with the capacity set to the wait
room size. When a new customer arrives at the barbershop, the code
receives it, checks if there channel is not full yet, and if that’s the
case adds the new customer to that channel, which will wake up the
barber goroutine in case it’s sleeping.
Again, notice that the channel where customers arrive (the “door” to the barbershop) is not buffered. Every customer arriving at the barbershop gets to talk to a “host”, which then tells them whether or not there’s room in the wait room.
The barber is this loop:
for {
c := <-barberCh
// cut hair here
c.Done()
}
and the customers are conceptually this loop:
for {
barbershop.BarbershopCh <- c
<-barbershop.WaitingCh
}
This approach might seem is a bit artificial, because there’s a loop producing customers. It’s not that far fetched, though, as it could correspond to a webserver taking requests and dispatching it to some worker goroutine, until a limit is hit and instead of servicing the request it replies with 503 (“service unavailable”), which is asking the client to retry in the future.
An alternative view is that there’s some kind of rate limiting (think
logging service), where multiple goroutines are sending requests and
it’s ok if some of those requests are not serviced. Since there’s a
single goroutine B reading BarbershopCh
, only one of the sending
goroutines C can proceed to the point where it’s waiting for the reply
from B. So the fact that here I’ve shown a loop fabricating clients does
not mean that this solution only works with such a setup.
I find this solution compares much favorably with the “standard” one based around mutexes and semaphores. I think it’s conceptually simpler to reason about, even if the two solutions are similar in spirit.
The full source, with minor modifications for presentation purposes, is here.
Next week: a variation of this same problem, Hilzer’s barbershop.