Skip to content

Commit 7a4400d

Browse files
committed
Sleeping barbers
1 parent af62788 commit 7a4400d

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
@@ -80,6 +80,7 @@ is distributed under the [ISC license](LICENSE.md).
8080
- [Programming with transactional data structures](#programming-with-transactional-data-structures)
8181
- [The dining philosophers problem](#the-dining-philosophers-problem)
8282
- [A transactional LRU cache](#a-transactional-lru-cache)
83+
- [The sleeping barbers problem](#the-sleeping-barbers-problem)
8384
- [Designing lock-free algorithms with k-CAS](#designing-lock-free-algorithms-with-k-cas)
8485
- [Understand performance](#understand-performance)
8586
- [Minimize accesses](#minimize-accesses)
@@ -1048,6 +1049,270 @@ val a_cache : (int, string) cache =
10481049
As an exercise, implement an operation to `remove` associations from a cache and
10491050
an operation to change the capacity of the cache.
10501051

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

10531318
The key benefit of k-CAS, or k-CAS-n-CMP, and transactions in particular, is

0 commit comments

Comments
 (0)