@@ -53,6 +53,8 @@ is distributed under the [ISC license](LICENSE.md).
5353 - [ Composing transactions] ( #composing-transactions )
5454 - [ Blocking transactions] ( #blocking-transactions )
5555 - [ A transactional lock-free leftist heap] ( #a-transactional-lock-free-leftist-heap )
56+ - [ Programming with transactional data structures] ( #programming-with-transactional-data-structures )
57+ - [ A transactional LRU cache] ( #a-transactional-lru-cache )
5658 - [ Programming with primitive operations] ( #programming-with-primitive-operations )
5759- [ Designing lock-free algorithms with k-CAS] ( #designing-lock-free-algorithms-with-k-cas )
5860 - [ Minimize accesses] ( #minimize-accesses )
@@ -713,6 +715,142 @@ Notice how we were able to use a `while` loop, rather than recursion, in
713715> lock-free heap implementation, but it was pretty straightforward to implement
714716> using k-CAS based on a textbook imperative implementation.
715717
718+ ### Programming with transactional data structures
719+
720+ When was the last time you implemented a non-trivial data structure or algorithm
721+ from scratch? For most professionals the answer might be along the lines of
722+ _ "when I took my data structures course at the university"_ or _ "when I
723+ interviewed for the software engineering position at Big Co"_ .
724+
725+ ** kcas** aims to be usable both
726+
727+ - for experts implementing correct and performant lock-free data structures, and
728+ - for everyone gluing together programs using such data structures.
729+
730+ Many of the examples in this introduction are data structures of some sort.
731+ However, implementing basic data structures from scratch is not something
732+ everyone should be doing every time they are writing concurrent programs.
733+ Instead programmers should be able to reuse carefully constructed data
734+ structures.
735+
736+ One source of ready-made data structures is the
737+ [ ** kcas_data** ] ( https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/index.html )
738+ package. Let's explore how we can leverage those data structures. Of course,
739+ first we need to ` #require ` the package and we'll also open it for convenience:
740+
741+ ``` ocaml
742+ # #require "kcas_data"
743+ # open Kcas_data
744+ ```
745+
746+ #### A transactional LRU cache
747+
748+ A LRU or least-recently-used cache is essentially a bounded association table.
749+ When the capacity of the cache is exceeded, some association is dropped. The LRU
750+ or least-recently-used policy is to drop the association that was accessed least
751+ recently.
752+
753+ A simple way to implement a LRU cache is to use a hash table to store the
754+ associations and a doubly-linked list to keep track of the order in which
755+ associations have been accessed. Whenever an association is accessed, the
756+ corresponding linked list node is added or moved to one end of the list. When
757+ the cache overflows, the association whose node is at the other end the list is
758+ removed.
759+
760+ The ** kcas_data** package conveniently provides a
761+ [ ` Hashtbl ` ] ( https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/Hashtbl/index.html )
762+ module providing a hash table implementation that mimics the Stdlib
763+ [ ` Hashtbl ` ] ( https://v2.ocaml.org/api/Hashtbl.html ) module and a
764+ [ ` Dllist ` ] ( https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/Dllist/index.html )
765+ providing a doubly-linked list implementation. We'll also keep track of the
766+ space in the cache using a separate shared memory location so that it is
767+ possible to change the capacity of the cache dynamically:
768+
769+ ``` ocaml
770+ type ('k, 'v) cache = {
771+ space: int Loc.t;
772+ table: ('k, 'k Dllist.node * 'v) Hashtbl.t;
773+ order: 'k Dllist.t;
774+ }
775+ ```
776+
777+ To create a cache we just create the data structures:
778+
779+ ``` ocaml
780+ # let cache ?hashed_type capacity =
781+ { space = Loc.make capacity;
782+ table = Hashtbl.create ?hashed_type ();
783+ order = Dllist.create () }
784+ val cache : ?hashed_type:'a Hashtbl.hashed_type -> int -> ('a, 'b) cache =
785+ <fun>
786+ ```
787+
788+ Note that above we just passed the optional ` hashed_type ` argument to the hash
789+ table constructor. The hash table
790+ [ ` create ` ] ( https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/Hashtbl/index.html#val-create )
791+ function takes some more optional arguments some of which might make sense to
792+ pass through.
793+
794+ To access an association in the cache we provide a ` get_opt ` operation
795+
796+ ``` ocaml
797+ # let get_opt ~xt {table; order; _} key =
798+ Hashtbl.Xt.find_opt ~xt table key
799+ |> Option.map @@ fun (node, value) ->
800+ Dllist.Xt.move_l ~xt node order;
801+ value
802+ val get_opt : xt:'a Xt.t -> ('b, 'c) cache -> 'b -> 'c option = <fun>
803+ ```
804+
805+ that, as explained previously, moves the node corresponding to the accessed
806+ association to the left end of the list.
807+
808+ To introduce associations we provide the ` set ` operation
809+
810+ ``` ocaml
811+ # let set ~xt {table; order; space; _} key value =
812+ let node =
813+ match Hashtbl.Xt.find_opt ~xt table key with
814+ | None ->
815+ if 0 = Xt.update ~xt space (fun n -> Int.max 0 (n-1)) then
816+ Dllist.Xt.take_opt_r ~xt order
817+ |> Option.iter (Hashtbl.Xt.remove ~xt table);
818+ Dllist.Xt.add_l ~xt key order
819+ | Some (node, _) ->
820+ Dllist.Xt.move_l ~xt node order;
821+ node
822+ in
823+ Hashtbl.Xt.replace ~xt table key (node, value)
824+ val set : xt:'a Xt.t -> ('b, 'c) cache -> 'b -> 'c -> unit = <fun>
825+ ```
826+
827+ that, like ` get_opt ` , either moves or adds the node of the accessed association
828+ to the left end of the list. In case a new association is added, the space is
829+ decremented. If there was no space, an association is first removed. As
830+ described previously, the association to remove is determined by removing the
831+ rightmost element from the list.
832+
833+ We can then test that the cache works as expected:
834+
835+ ``` ocaml
836+ # let a_cache : (int, string) cache = cache 2
837+ val a_cache : (int, string) cache =
838+ {space = <abstr>; table = <abstr>; order = <abstr>}
839+ # Xt.commit { tx = set a_cache 101 "basics" }
840+ - : unit = ()
841+ # Xt.commit { tx = set a_cache 42 "answer" }
842+ - : unit = ()
843+ # Xt.commit { tx = get_opt a_cache 101 }
844+ - : string option = Some "basics"
845+ # Xt.commit { tx = set a_cache 2023 "year" }
846+ - : unit = ()
847+ # Xt.commit { tx = get_opt a_cache 42 }
848+ - : string option = None
849+ ```
850+
851+ As an exercise, implement an operation to ` remove ` associations from a cache and
852+ an operation to change the capacity of the cache.
853+
716854### Programming with primitive operations
717855
718856The [ ` Op ` ] ( https://ocaml-multicore.github.io/kcas/doc/kcas/Kcas/Op/index.html )
@@ -1392,7 +1530,7 @@ accessing the association list location corresponding to specified key:
13921530# let access ~xt basic_hashtbl key =
13931531 let data = Xt.get ~xt basic_hashtbl.data in
13941532 let n = Array.length data in
1395- let i = Hashtbl.hash key mod n in
1533+ let i = Stdlib. Hashtbl.hash key mod n in
13961534 data.(i)
13971535val access :
13981536 xt:'a Xt.t -> ('b, 'c) basic_hashtbl -> 'd -> ('b * 'c Loc.t) list Loc.t =
@@ -1457,7 +1595,7 @@ operation:
14571595 |> Array.iter @@ fun assoc_loc ->
14581596 Xt.get ~xt assoc_loc
14591597 |> List.iter @@ fun ((key, _) as bucket) ->
1460- let i = Hashtbl.hash key mod new_capacity in
1598+ let i = Stdlib. Hashtbl.hash key mod new_capacity in
14611599 Xt.modify ~xt new_data.(i) (List.cons bucket)
14621600val rehash : xt:'a Xt.t -> ('b, 'c) basic_hashtbl -> int -> unit = <fun>
14631601```
@@ -1743,11 +1881,6 @@ implementations that are conveniently provided by the
17431881[ ** kcas_data** ] ( https://ocaml-multicore.github.io/kcas/doc/kcas_data/Kcas_data/index.html )
17441882package.
17451883
1746- ``` ocaml
1747- # #require "kcas_data"
1748- # open Kcas_data
1749- ```
1750-
17511884Here is the full toy scheduler module:
17521885
17531886``` ocaml
0 commit comments