@@ -81,6 +81,7 @@ is distributed under the [ISC license](LICENSE.md).
81
81
- [ Programming with transactional data structures] ( #programming-with-transactional-data-structures )
82
82
- [ The dining philosophers problem] ( #the-dining-philosophers-problem )
83
83
- [ A transactional LRU cache] ( #a-transactional-lru-cache )
84
+ - [ The sleeping barbers problem] ( #the-sleeping-barbers-problem )
84
85
- [ Designing lock-free algorithms with k-CAS] ( #designing-lock-free-algorithms-with-k-cas )
85
86
- [ Understand performance] ( #understand-performance )
86
87
- [ Minimize accesses] ( #minimize-accesses )
@@ -1049,6 +1050,270 @@ val a_cache : (int, string) cache =
1049
1050
As an exercise, implement an operation to ` remove ` associations from a cache and
1050
1051
an operation to change the capacity of the cache.
1051
1052
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
+
1052
1317
## Designing lock-free algorithms with k-CAS
1053
1318
1054
1319
The key benefit of k-CAS, or k-CAS-n-CMP, and transactions in particular, is
0 commit comments