|
| 1 | +(** This is a library of compositional lock-free data structures and primitives |
| 2 | + for communication and synchronization implemented using {!Kcas}. |
| 3 | +
|
| 4 | + All data structure implementations in this library are concurrency and |
| 5 | + parallelism safe and should strive to provide the following guarantees: |
| 6 | +
|
| 7 | + - Provided operations are {i strictly serializable} (i.e. both |
| 8 | + {{:https://en.wikipedia.org/wiki/Linearizability}linerizable} and |
| 9 | + {{:https://en.wikipedia.org/wiki/Serializability}serializable}). |
| 10 | + - Provided operations are efficient, either |
| 11 | + ({{:https://en.wikipedia.org/wiki/Amortized_analysis}amortized}) constant |
| 12 | + time, [O(1)], or logarithmic time, [O(log(n))]. |
| 13 | + - Provided operations are |
| 14 | + {{:https://en.wikipedia.org/wiki/Non-blocking_algorithm#Lock-freedom}lock-free} |
| 15 | + and designed to avoid |
| 16 | + {{:https://en.wikipedia.org/wiki/Starvation_(computer_science)}starvation} |
| 17 | + under moderate contention. |
| 18 | + - Provided read-only operations scale perfectly when only read-only |
| 19 | + operations are performed in parallel. |
| 20 | +
|
| 21 | + Unobvious exceptions to the above guarantees should be clearly and |
| 22 | + explicitly documented. |
| 23 | +
|
| 24 | + The main feature of these data structure implementations is their |
| 25 | + compositionality. If your application does not need compositionality, then |
| 26 | + other concurrency and parallelism safe data structure libraries may |
| 27 | + potentially offer better performance. |
| 28 | +
|
| 29 | + But why should you care about composability? |
| 30 | +
|
| 31 | + As an example, consider the implementation of a least-recently-used (LRU) |
| 32 | + cache or a bounded associative map. A simple sequential approach to |
| 33 | + implement a LRU cache is to use a hash table and a doubly-linked list and |
| 34 | + keep track of the amount of space in the cache: |
| 35 | +
|
| 36 | + {[ |
| 37 | + type ('k, 'v) cache = |
| 38 | + { space: int Loc.t; |
| 39 | + table: ('k, 'k Dllist.node * 'v) Hashtbl.t; |
| 40 | + order: 'k Dllist.t } |
| 41 | + ]} |
| 42 | +
|
| 43 | + On a cache lookup the doubly-linked list node corresponding to the accessed |
| 44 | + key is moved to the left end of the list: |
| 45 | +
|
| 46 | + {[ |
| 47 | + let get_opt {table; order; _} key = |
| 48 | + Hashtbl.find_opt table key |
| 49 | + |> Option.map @@ fun (node, datum) -> |
| 50 | + Dllist.move_l node order; datum |
| 51 | + ]} |
| 52 | +
|
| 53 | + On a cache update, in case of overflow, the association corresponding to the |
| 54 | + node on the right end of the list is dropped: |
| 55 | +
|
| 56 | + {[ |
| 57 | + let set {table; order; space; _} key datum = |
| 58 | + let node = |
| 59 | + match Hashtbl.find_opt table key with |
| 60 | + | None -> |
| 61 | + if 0 = Loc.update space (fun n -> max 0 (n-1)) |
| 62 | + then Dllist.take_opt_r order |
| 63 | + |> Option.iter (Hashtbl.remove table); |
| 64 | + Dllist.add_l key order |
| 65 | + | Some (node, _) -> Dllist.move_l node order; node |
| 66 | + in |
| 67 | + Hashtbl.replace table key (node, datum) |
| 68 | + ]} |
| 69 | +
|
| 70 | + Sequential algorithms such as the above are so common that one does not even |
| 71 | + think about them. Unfortunately, in a concurrent setting the above doesn't |
| 72 | + work even if the individual operations on lists and hash tables were atomic |
| 73 | + as they are in this library. |
| 74 | +
|
| 75 | + But how would one make the operations on a cache atomic as a whole? As |
| 76 | + explained by Maurice Herlihy in one of his talks on |
| 77 | + {{:https://youtu.be/ZkUrl8BZHjk?t=1503} Transactional Memory} adding locks |
| 78 | + to protect the atomicity of the operation is far from trivial. |
| 79 | +
|
| 80 | + Fortunately, rather than having to e.g. wrap the cache implementation behind |
| 81 | + a {{:https://en.wikipedia.org/wiki/Lock_(computer_science)} mutex} and make |
| 82 | + another individually atomic yet uncomposable data structure, or having to |
| 83 | + learn a completely different programming model and rewrite the cache |
| 84 | + implementation, we can use the transactional programming model provided by |
| 85 | + the {!Kcas} library and the transactional data structures provided by this |
| 86 | + library to trivially convert the previous implementation to a lock-free |
| 87 | + composable transactional data structure. |
| 88 | +
|
| 89 | + To make it so, we simply use transactional versions, [*.Xt.*], of operations |
| 90 | + on the data structures and explicitly pass a transaction log, [~xt], to the |
| 91 | + operations. For the [get_opt] operation we end up with |
| 92 | +
|
| 93 | + {[ |
| 94 | + let get_opt ~xt {table; order; _} key = |
| 95 | + Hashtbl.Xt.find_opt ~xt table key |
| 96 | + |> Option.map @@ fun (node, datum) -> |
| 97 | + Dllist.Xt.move_l ~xt node order; datum |
| 98 | + ]} |
| 99 | +
|
| 100 | + and the [set] operation is just as easy to convert to a transactional |
| 101 | + version. One way to think about transactions is that they give us back the |
| 102 | + ability to compose programs such as the above. *) |
| 103 | + |
| 104 | +(** {1 [Stdlib] style data structures} |
| 105 | +
|
| 106 | + The data structures in this section are designed to closely mimic the |
| 107 | + corresponding unsynchronized data structures in the OCaml [Stdlib]. Each of |
| 108 | + these provide a non-compositional, but concurrency and parallelism safe, |
| 109 | + interface that is close to the [Stdlib] equivalent. Additionally, |
| 110 | + compositional transactional interfaces are provided for some operations. |
| 111 | +
|
| 112 | + These implementations will use more space than the corresponding [Stdlib] |
| 113 | + data structures. Performance, when accessed concurrently, should be |
| 114 | + competitive or superior compared to naïve locking. *) |
| 115 | + |
| 116 | +module Hashtbl = Hashtbl |
| 117 | +module Queue = Queue |
| 118 | +module Stack = Stack |
| 119 | + |
| 120 | +(** {1 Communication and synchronization primitives} *) |
| 121 | + |
| 122 | +module Mvar = Mvar |
| 123 | +module Promise = Promise |
| 124 | + |
| 125 | +(** {1 Linked data structures} *) |
| 126 | + |
| 127 | +module Dllist = Dllist |
| 128 | + |
| 129 | +(** {1 Utilities} *) |
| 130 | + |
| 131 | +module Accumulator = Accumulator |
0 commit comments