Overthunk

Complex Interactions - A Tale of Concurrency

Jun 27, 2020


Contents

Expectations

My goal for today was to finish reading Chapter 6 and attempt a couple of the end of the chapter exercises. However, I did not have high hopes that I would be able to do this since I had to travel (which takes up quite a chunk of time). The challenge for myself was seeing how efficiently I could learn complex topics in a short period of time.

What I learned

Man Chapter 6 is incredibly beefy! It handles concurrency, deferred execution, state management, and parallelism. Quite the gamut of topics for a single chapter. I had been quite interested in reaching this part since I’d heard that Clojure’s concurrency story was extremely well done.

Let’s check out some salient parts from Chapter 6. However, before we do that, I am including a wonderful table from the chapter for reference.

TypeMutabilityReadsUpdatesEvaluationScope
SymbolImmutableTransparentLexical
VarMutableTransparentUnrestrictedGlobal/Dynamic
DelayMutableBlockingOnceonlyLazy
FutureMutableBlockingOnceonlyParallel
PromiseMutableBlockingOnceonly
AtomMutableNonblockingLinearizable
RefMutableNonblockingSerializable

Delay

delay is a way to defer evaluation until it is needed.

(def peaceout
    (delay
        (println "Peace Out")
        (* 42 15)))

The call to delay returns a Delay object. This object is a reference to the expressions contained within delay that we want to be evaluated at a later time. Now the question is how do we get the expressions within the Delay object to evaluate.

Well since the object is a reference to the expressions, maybe we can just … dereference?

(deref peaceout)

; "Peace Out"
; 630

The deref form takes a Delay Object and evaluates the expressions within it. There is an interesting distinction between functions and delays. Despite them both deferring execution, functions execute their body on every invocation. On the other hand, once a delay has been evaluated, it remembers the value (cached) and returns this value on future derefs.

Futures

Futures are just delays executed on different threads. In a world where large number of cores and threads are becoming the norm for even consumer devices, it makes sense to have constructs that allow for parallel computation on different threads. Futures behave pretty similarly to Delays in other aspects (deref, calling the future object directly returns a reference, caching of result).

Promises

Promises (like in real life) are just contracts for expected behavior. When we construct a Promise, it is initially empty.

user=> (def box (promise))
#'user/box
user=> box
#object[clojure.core$promise$reify__8501 0x15deb1dc {:status :pending, :val nil}]

We can see that the Promise’s status is :pending. It is expecting a value to be placed in it. So if we try to deref an empty Promise, we end up blocking the current thread since the promise is waiting for something to happen. We need to deliver!.

(deliver box :soba-noodels)

Now trying to deref box will give us the value that we delivered. However once we’ve delivered something to a Promise, we can’t rescind it and change what we’ve delivered. We must re-bind an empty promise to the box symbol and re-deliver a value into it.

Promise is a Clojure concurrency Primitive. This means that trying to read what’s in Promise will always wait until there is some value to read. This allows us to synchronize a program that uses other parallelism or concurrency features.

Takeaways

Clojure has excellent core functions that make it almost trivial to use parallelism and concurrency in your programs. I’m almost excited to try building or working on complex projects that use these features now to see how these play out in the real world.

There are a few more cool things that Chapter 6 covered. But I’m going to cover those topics tomorrow with a fresh and hopefully rested mind. It’s been quite a busy day today and I definitely need some shut eye

(deref wait)