Clojure for the brave and the true¶
Parallel demons concurrency¶
- Reference cell: Read and write a shared state.
- Mutual exclusion: Example with writing a log with several processes, e.g. concatenate "ab" and "cd" in a file results in "acbd".
- Deadlock: On a table, every one has to take the left and right stick and the same time. However, there is only one stick per person. Leading to a deadlock.
Solutions to these are future
(new thread), delay
(like future but
executed only at deref
time), promise
(empty memory location shared
to all thread that will receive value only once with deliver
). The
advantage is all the execution are cached. Note: deference is done
with either the defer
function or the @
sign in front of the
variable.
References type concurrency epochal_time_model¶
-
atom
are identities that can be set and shared by multiple threads. They use a set and compare algorithm, i.e. theswap!
function modifies the value of the atom variable only if its values did not change between the beginning and the end of the transaction.swap!
andreset!
are the main function to work with atoms. -
watch
are function with four arguments: a key (a keyword for identifying the process calling the watcher), a reference variable being watched, the old-state, and the new state.(defn f [key watched old-state new-state] nil)
A watcher function is attached to a reference type (e.g an atom) with the
add-watch
function having the following signature:(def counter (atom 0)) (add-watch counter :watching-counter watch-fn)
-
Validators are functions that can check if new states are valid. They take as argument the atom and return a boolean. They are added to the atom as follow
(defn bigger-than-1 [x] (or (> x 1) (throw (IllegalStateException. "That's too small")))) (def account (atom 2 :validator bigger-than-1)) (swap! account inc) (swap! account - 10) ;; Throw an error
-
ref
type are the ACI in the ACID accronym (atomic, consistent and isolated) and use STM. It means that either the operations between two refs happened correctly, or the transaction is aborted.alter
anddosync
are the key functions.- In a transaction (that is the body of
dosync
), everyref
keep their state to the transaction (invisible to outside threads) and when the transaction tries to commit, everyref
checks if the value has been altered by other threads. - If any of them has been change, then none of the
ref
are updated, and the transaction restart with the new value and commits only when the initial states has not been /alter/ed by other processes.
commute
also allow to change the state of a ref. However, at transaction time, if ref states have been altered, only thecommute
part is run again with the new states, which might lead to inconsistent state, but increased performance.ensure
function protects refs from being modified by other transaction. This is helpful, when a transaction must modify only one refs, but the other related refs must not be altered by other transaction. - In a transaction (that is the body of
-
vars
are associations between symbols and objects.^:dynamic
is a keyword indef
to signal to clojure that a vars is dynamic. Varnames are enclosed around*
(e.g.*user-email*
) to show to other programmers that the variable is dynamic.bindings
is a dynamiclet
. Dynamic vars are often use to name a resource that one ore more functions target.set!
allows to change the state of the dynamic vars.alter-var-root
allows to rebind a immutable vars (which is unadvised), andwith-redefs
allows to create local binding for testing. -
pmap
and the followingppmap
can be used to execute parallel task:(defn ppmap "Partitioned pmap, for grouping map ops together to make parallel overehead worthwile" [grain-size f & colls] (apply concat (apply pmap (fn [& pgroups] (doall (apply map f pgroups))) (map (partial partition-all grain-size) colls))))
core.async and channels core_async¶
-
chan
creates a channel. And channel communicate through messages. One can put and take message. Processes wait for completion of their message. Process: Wait and do nothing until successful completion of either put or take from a channel. After success of the operation, continue. -
go
and their blocks (go blocks) runs separately on a concurrent thread.go
creates a process (i.e. its go block), which runs a pool of threads equal two plus the number of machines cores (avoiding the overhead of creating threads). Eachgo block
only live until it reach the ends of its body. -
<!
and<!!
are the take function. It listen to the channel and wait until an another process puts a value in the channel which the take function returns.>!
and>!!
are the put function which always return true. It provides a message to a channel and wait until the message to be taken by another process before releasing resources. The number of!
in the operation depends if one is inside a go block (one!
) or not (two!
). Blocking and parking waiting are key to understand the number of!
. Parking wait allows a thread to handle several process (and this is only possible in a go block). When one of the process starts to wait, the thread put it aside and starts an another process until it starts to wait, and so on. Usepoll!
andoffer!
to have non blocking channel interactions in the REPL. -
Channel buffers are created as following:
(def buffer-size 2) (def channel-buffer (chan buffer-size))
This means we can create 2 values without waiting for a response.
sliding-buffer
(FIFO) anddropping-buffer
(LIFO) can be used to discard channel message without blocking. -
close!
closes channel. A closed channel does not accept any puts anymore and after all the values have been retrieved, the subsequent takes returnnil
. -
alts!!
lets us use the result of the first successful channel operation among a collection of channel operations. The elegant solution withalts!!
is one can define a timeout(let [[message channel] (alts!! [c1 c2 (timout 20)])] ;; c1 and c2 are predefined channels. (println message))
if the timeout is the first to finish than
message
isnil
. Seealt!
macro as well. -
Queues and pipelines (escaping the callback hell) are common patterns.
Abstraction and polymorphism¶
-
Multimethods
(defmulti method-name (fn [x] (:type x))) ;; or simplty :type, can be more complicated as well (defmethod method-name :hello [x] "Hello") (defmethod method-name :good-bye [x] "Good-bye") (defmethod method-name :default [x] "I don't know you") (method-name {:type :hello}) ; => Hello (method-name {:type :good-bye}) ; Good-bye (method-name {:type :what?}) ; => I don't know you
One can also create hierarchies with
derive
and namespace keywords. -
A protocol allows to make dispatch by the type of the first argument and it is a collection of polymorphic operations (unlike multimethod which is just one function). Methods from protocols can not have a
& rest
argument. Key functions aredefprotocol
,extend-type
,extend-protocol
(for specifying for several type at once).- Caveat: methods from protocols are property of the namespace and not from the object.
-
Records are extension of
hash-map
.(defrecord WereWolf [name title]) (WereWolf. "David" "Master") (->WereWolf "David" "Master") (map->WereWolf {:name "David" :title "Master"})
On has to use the
:import:
statement in thens
macro in order to import records. One can access field through the keyword or the dot.
macro.(.name (WereWolf. "David" "Master")) ; "David" (:title (WereWolf. "David" "Master")) ; "Master"
Any function on map works on record (although they do not retain their class if one
dissoc
orassoc
them). Here is how one could extend a protocol.(defprotocol WereCreature "Awesom Were" (full-moon-behavior [x] "Full-moon behavior")) (defrecord WereWolf [name title] WereCreature (full-moon-behavior [x] (str name " will kill everyone"))) (full-moon-behavior (WereWolf. "David" "Master"))
-
deftype
,reify
,proxy
. reify is about implementing an anonymous protocol at runtime.