A Future of Delays and Promises
Please note: this Medium post is about how to deal with concurrency and parallel programming techniques in Clojure. Contrary to the title, it is not a bleak opinion piece on the political state of our nation (although, to be fair, how can you not be optimistic after Michelle Obama’s DNC speech?!)
I never experienced so much amusement in reading a technical book, about programming no less, than when I read the opening of chapter nine of Clojure for the Brave and True. If I wasn’t learning Clojure, I would — just to read this book and because of this book.
Artfully extracting Lady Gaga’s lyric, author Daniel Higginbotham explains what it means to managing multiple tasks versus executing tasks simultaneously. In a non-artful way, I will bullet point these crucial terms:
- Concurrency refers to managing more than one task at the same time, without any implication about implementation.
- Interleaving means switching between two or more tasks at the same time.
- Parallelism refers to executing more than one task at the same.
- Distribution is a special version of parallel computing where processors are in different computers and tasks are distributed to computers over a network.
In Clojure, tasks can be performed concurrently by placing them on Java Virtual Machine (JVM) threads. More about threads: they’re subprograms that execute their own set of instructions and have access to their program’s state. A program has multiple threads. A thread can spawn a new thread to execute tasks concurrently. When this happens, the processor can execute the two threads nondeterministically (meaning that there are multiple possibilities for the order of instructions). Not knowing the execution order can be problematic, as different execution orders can lead to different results. Yikes.
The first main challenge of concurrent program is the reference cell problem. It occurs when two threads can read and write to the same location, and the value at the location depends on the order of the reads and writes.
The second challenge, mutual exclusion, occurs when two threads have write access to a file and end up interleaving write instructions. The issue is that neither thread can claim exclusive write access to the file.
The final challenge is the deadlock problem. In this scenario, our program becomes stalled by threads that are blocked indefinitely, because they are relying on other threads to become available.
When we write serial code, as we do in Clojure, we’re essentially binding the following: a task’s definition, its execution, and requiring its result. Futures, delays, and promises allow us to separate these three things and identify when couplings aren’t necessarily.
Futures define a task and place it on another thread without requiring the result right away. Futures are created with the `future` macro. See this example below:
(future (Thread/sleep 5000)
(println "I'll print after 5 seconds"))
(println "I'll print immediately")
Using `future` allows us to hand off the expression into a new thread and immediately return a string, followed by another string five seconds later. The `future` function returns a reference value that can be used to request the result. If the future isn’t done computing the result, there will be a wait time; this is called dereferencing the future and can be done with the `deref` function or `@` reader macro. You can even use `realized?` to see if an expression is done running!
Solving the issue of mutual exclusion, delays are a technique to define a task without having to execute it or require the result. You create a delay by calling `delay`. This example will not print anything until you use `force`. A delay is run only once and its result is cached. Any dereferencing that happens after will just return my name without printing anything.
(delay (let [name "Malina"]
(println "Hi, my name is " name)
; => Hi, my name is Malina
; => "Malina"
The last solution is promises, which I’ve encountered when working with Ember.js, and helps solve the issue of reference cells. Promises allow you to express that you expect a result, without defining when and how. Sounds like a typical promise (kidding!). You create promises using the `promise` keyword and deliver a result to them using `deliver`. A simple example below:
(def my-promise (promise))
(deliver my-promise (+ 1 2))
; => 3