Skip to content

Commit 49e66a7

Browse files
committed
Sleeping barbers
1 parent 055bb9f commit 49e66a7

File tree

1 file changed

+265
-0
lines changed

1 file changed

+265
-0
lines changed

README.md

+265
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ is distributed under the [ISC license](LICENSE.md).
7575
- [Programming with transactional data structures](#programming-with-transactional-data-structures)
7676
- [The dining philosophers problem](#the-dining-philosophers-problem)
7777
- [A transactional LRU cache](#a-transactional-lru-cache)
78+
- [The sleeping barbers problem](#the-sleeping-barbers-problem)
7879
- [Programming with primitive operations](#programming-with-primitive-operations)
7980
- [Designing lock-free algorithms with k-CAS](#designing-lock-free-algorithms-with-k-cas)
8081
- [Understand performance](#understand-performance)
@@ -1049,6 +1050,270 @@ val a_cache : (int, string) cache =
10491050
As an exercise, implement an operation to `remove` associations from a cache and
10501051
an operation to change the capacity of the cache.
10511052

1053+
#### The sleeping barbers problem
1054+
1055+
The
1056+
[sleeping barber problem](https://en.wikipedia.org/wiki/Sleeping_barber_problem)
1057+
is another classic communication and synchronization problem. Let's write a
1058+
solution using **kcas**.
1059+
1060+
There are
1061+
[many ways to solve the problem](https://en.wikipedia.org/wiki/Sleeping_barber_problem#Solutions)
1062+
and, in particular, there are concise and subtle implementations using
1063+
semaphores or mutexes. Instead of transliterating a solution using semaphores,
1064+
our approach uses queues and other concurrent data structures. We also solve the
1065+
generalized problem with multiple barbers and we also implement a mechanism to
1066+
close the barbershop. In addition, we abstract the concept of a barbershop,
1067+
where barbers and customers interact. All of this makes our solution longer than
1068+
the well known semaphore based solution. On the other hand, one might argue that
1069+
our solution is a more direct transliteration of the problem. Our solution also
1070+
avoids the starvation problem by using queues.
1071+
1072+
Let's begin by abstracting customer
1073+
1074+
```ocaml
1075+
type customer = {
1076+
notify_hair_has_been_cut : 'x.xt:'x Xt.t -> unit;
1077+
}
1078+
```
1079+
1080+
and barber
1081+
1082+
```ocaml
1083+
type barber = {
1084+
wake_up : 'x.xt:'x Xt.t -> customer -> unit;
1085+
}
1086+
```
1087+
1088+
actors. The idea is that barbers notify customers after finishing their haircut
1089+
and, adhering to the problem description, customers wake up sleeping barbers.
1090+
1091+
A barbershop consists of any number of barbers and waiting customers and can be
1092+
marked as closed:
1093+
1094+
```ocaml
1095+
type barbershop = {
1096+
sleeping_barbers : barber Queue.t;
1097+
waiting_customers : customer Queue.t;
1098+
is_closed : bool Loc.t;
1099+
}
1100+
```
1101+
1102+
The barbershop constructor does not limit the number of barbers, which are
1103+
assumed to bring their own chairs, but does require a specification of the
1104+
number of waiting room chairs for customers:
1105+
1106+
```ocaml
1107+
# let barbershop ~num_waiting_chairs =
1108+
let sleeping_barbers = Queue.create ()
1109+
and waiting_customers = Queue.create ~capacity:num_waiting_chairs ()
1110+
and is_closed = Loc.make false in
1111+
{ sleeping_barbers; waiting_customers; is_closed }
1112+
val barbershop : num_waiting_chairs:int -> barbershop = <fun>
1113+
```
1114+
1115+
Although the `barbershop` type is not abstract, we treat it as such, so we
1116+
provide a transactional predicate to check whether the barbershop is closed or
1117+
not:
1118+
1119+
```ocaml
1120+
# let is_closed ~xt bs = Xt.get ~xt bs.is_closed
1121+
val is_closed : xt:'a Xt.t -> barbershop -> bool = <fun>
1122+
```
1123+
1124+
To `close` a barbershop we set the `is_closed` location to `true` and clear both
1125+
the sleeping barbers and waiting customers queues:
1126+
1127+
```ocaml
1128+
# let close ~xt bs =
1129+
Xt.set ~xt bs.is_closed true;
1130+
Queue.Xt.clear ~xt bs.sleeping_barbers;
1131+
Queue.Xt.clear ~xt bs.waiting_customers
1132+
val close : xt:'a Xt.t -> barbershop -> unit = <fun>
1133+
```
1134+
1135+
A barber can try to get a customer sitting on a waiting room chair:
1136+
1137+
```ocaml
1138+
# let get_sitting_customer_opt ~xt bs =
1139+
Queue.Xt.take_opt ~xt bs.waiting_customers
1140+
val get_sitting_customer_opt : xt:'a Xt.t -> barbershop -> customer option =
1141+
<fun>
1142+
```
1143+
1144+
Or may go to sleep on the barber's own chair:
1145+
1146+
```ocaml
1147+
# let sleep ~xt bs barber =
1148+
if not (is_closed ~xt bs) then
1149+
Queue.Xt.add ~xt barber bs.sleeping_barbers
1150+
val sleep : xt:'a Xt.t -> barbershop -> barber -> unit = <fun>
1151+
```
1152+
1153+
Note that the `sleep` transaction uses the `is_closed` predicate. Barbers, as
1154+
well as customers, must leave the shop in case it is closed.
1155+
1156+
A customer can try to find a sleeping barber:
1157+
1158+
```ocaml
1159+
# let get_sleeping_barber_opt ~xt bs =
1160+
Queue.Xt.take_opt ~xt bs.sleeping_barbers
1161+
val get_sleeping_barber_opt : xt:'a Xt.t -> barbershop -> barber option =
1162+
<fun>
1163+
```
1164+
1165+
Or sit on a waiting room chair:
1166+
1167+
```ocaml
1168+
# let try_sitting ~xt bs customer =
1169+
not (is_closed ~xt bs) &&
1170+
Queue.Xt.try_add ~xt customer bs.waiting_customers
1171+
val try_sitting : xt:'a Xt.t -> barbershop -> customer -> bool = <fun>
1172+
```
1173+
1174+
The above `try_sitting` transaction is non-blocking. In case the
1175+
`waiting_customers` queue is full, it will return `false`. With the `customer`
1176+
actor implementation we'll look at shortly this would mean that customers would
1177+
busy-wait, which works, but potentially wastes energy. Here is a blocking
1178+
version of `try_sitting`:
1179+
1180+
```ocaml
1181+
# let try_sitting ~xt bs customer =
1182+
not (is_closed ~xt bs) &&
1183+
begin
1184+
Queue.Xt.add ~xt customer bs.waiting_customers;
1185+
true
1186+
end
1187+
val try_sitting : xt:'a Xt.t -> barbershop -> customer -> bool = <fun>
1188+
```
1189+
1190+
Both of the above `try_sitting` transactions work with the `customer` actor
1191+
we'll see shortly, but with the latter blocking version we avoid busy-wait.
1192+
1193+
The above constitutes the barbershop abstraction which is a kind of passive
1194+
concurrent data structure. Let's then implement the active participants of the
1195+
problem.
1196+
1197+
A customer tries to get a haircut. When a customer enter the barbershop he first
1198+
tries to find a sleeping barber. If none is available, the customer then tries
1199+
to sit on a waiting room chair. If both fail, then the customer has no option
1200+
except to retry. Otherwise the customer waits to get a haircut. If the shop is
1201+
closed, the customer exits. Here is the `customer` actor:
1202+
1203+
```ocaml
1204+
# let customer shop cuts =
1205+
let clean = Mvar.create None in
1206+
let self = { notify_hair_has_been_cut = Mvar.Xt.put clean true } in
1207+
while not (Xt.commit { tx = is_closed shop }) do
1208+
let get_barber_opt ~xt =
1209+
match get_sleeping_barber_opt ~xt shop with
1210+
| None ->
1211+
try_sitting ~xt shop self
1212+
| Some barber ->
1213+
barber.wake_up ~xt self;
1214+
true
1215+
in
1216+
if Xt.commit { tx = get_barber_opt } then
1217+
let try_await_haircut ~xt =
1218+
not (is_closed ~xt shop) &&
1219+
Mvar.Xt.take ~xt clean
1220+
in
1221+
if Xt.commit { tx = try_await_haircut } then
1222+
Loc.incr cuts
1223+
done
1224+
val customer : barbershop -> int Loc.t -> unit = <fun>
1225+
```
1226+
1227+
A barber tries to get a customer to give a haircut. A barber first looks for a
1228+
customer from the waiting room. If none is available, the barber goes to sleep
1229+
waiting for a wakeup from a customer. After obtaining a customer in either way,
1230+
the barber gives a haircut to the customer. Otherwise the shop must be closed
1231+
and the barber exits. Here is the `barber` actor:
1232+
1233+
```ocaml
1234+
# let barber shop cuts =
1235+
let customer = Mvar.create None in
1236+
let self = { wake_up = Mvar.Xt.put customer } in
1237+
while not (Xt.commit { tx = is_closed shop }) do
1238+
let cut customer =
1239+
Xt.commit { tx = customer.notify_hair_has_been_cut };
1240+
Loc.incr cuts
1241+
in
1242+
let get_customer_opt ~xt =
1243+
match get_sitting_customer_opt ~xt shop with
1244+
| Some _ as some -> some
1245+
| None ->
1246+
sleep ~xt shop self;
1247+
None
1248+
in
1249+
match Xt.commit { tx = get_customer_opt } with
1250+
| Some customer -> cut customer
1251+
| None ->
1252+
let await_wakeup_opt ~xt =
1253+
if is_closed ~xt shop then None
1254+
else Some (Mvar.Xt.take ~xt customer)
1255+
in
1256+
match Xt.commit { tx = await_wakeup_opt } with
1257+
| Some customer -> cut customer
1258+
| None -> ()
1259+
done
1260+
val barber : barbershop -> int Loc.t -> unit = <fun>
1261+
```
1262+
1263+
To run the problem, a barbershop is created with given number of waiting room
1264+
chairs, is populated by given number of barbers, and a given number of customers
1265+
are spawned. Once each barber has given and each customer has received a given
1266+
number of haircuts the shop is closed. This termination condition seeks to
1267+
demonstrate that no actor is starved. Here is the `sleeping_barbers` setup:
1268+
1269+
```ocaml
1270+
# let sleeping_barbers ~barbers
1271+
~num_waiting_chairs
1272+
~customers
1273+
~cuts_per_actor =
1274+
assert (0 < barbers
1275+
&& 0 <= num_waiting_chairs
1276+
&& 0 <= customers
1277+
&& 0 <= cuts_per_actor);
1278+
let shop = barbershop ~num_waiting_chairs in
1279+
let barbers = Array.init barbers @@ fun _ ->
1280+
let cuts = Loc.make 0 in
1281+
(cuts, Domain.spawn @@ (fun () -> barber shop cuts))
1282+
and customers = Array.init customers @@ fun _ ->
1283+
let cuts = Loc.make 0 in
1284+
(cuts, Domain.spawn @@ (fun () -> customer shop cuts))
1285+
in
1286+
let agents = Array.append barbers customers in
1287+
while agents
1288+
|> Array.map fst
1289+
|> Array.exists @@ fun c ->
1290+
Loc.get c < cuts_per_actor do
1291+
Domain.cpu_relax ()
1292+
done;
1293+
Xt.commit { tx = close shop };
1294+
agents
1295+
|> Array.map snd
1296+
|> Array.iter Domain.join
1297+
val sleeping_barbers :
1298+
barbers:int ->
1299+
num_waiting_chairs:int -> customers:int -> cuts_per_actor:int -> unit =
1300+
<fun>
1301+
```
1302+
1303+
Finally, let's try our solution:
1304+
1305+
```ocaml
1306+
# sleeping_barbers ~barbers:2
1307+
~num_waiting_chairs:1
1308+
~customers:4
1309+
~cuts_per_actor:10
1310+
- : unit = ()
1311+
```
1312+
1313+
Like mentioned in the beginning, this is not the most concise solution of the
1314+
sleeping barbers problem, but hopefully this solution can be understood
1315+
relatively easily with respect to the problem description.
1316+
10521317
### Programming with primitive operations
10531318

10541319
In addition to the transactional interface, **Kcas** also provides the

0 commit comments

Comments
 (0)