Chapter 6. State and the concurrent world

This chapter covers

  • The problems with mutable state
  • Clojure’s approach to state
  • Refs, agents, atoms, and vars
  • Futures and promises

State—you’re doing it wrong.

Rich Hickey[1]

1

From a presentation at the Boston Lisp meeting, 2012, http://www.youtube.com/watch?v=7mbcYxHO0nM&t=00h21m04s.

The preceding quote is from a presentation by Rich Hickey in which he discusses Clojure’s approach to concurrency and state. He means that most languages use an approach to modeling state that doesn’t work very well. To be precise, it used to work when computers were less powerful and ran programs in a single-threaded fashion, but in today’s world of increasingly multicore and multi-CPU computers, the model has broken down. This is evidenced by the difficulty of writing bug-free multithreaded code in typical object-oriented (OO) languages like Java and C++. Still, programmers continue to make the attempt, because today’s demands on software require that it take advantage of all available CPU cores. As software requirements grow in complexity, parallelism is becoming an implicit requirement.

This chapter is about concurrent programs and the problems they face in dealing with state. We’ll first examine what these problems are and then look at the traditional solutions. We’ll then look at Clojure’s approach to dealing with these issues and show that when trying to solve difficult problems, it’s sometimes worth starting with a fresh slate.

6.1. The problem with state

State is the current set of values associated with things in a program. For example, a payroll program might deal with employee objects. Each employee object represents the state of the employee, and every program usually has a lot of such state. There’s no problem with state, per se, or even with mutating state. The real world is full of perceived changes: people change, plans change, the weather changes, and the balance in a bank account changes. The problem occurs when concurrent (multithreaded) programs share this sort of state among different threads and then attempt to make updates to it. When the illusion of single-threaded execution breaks down, the code encounters all manner of inconsistent data. In this section, we’ll look at a solution to this problem. But before we do, let’s recap the issues faced by concurrent programs operating on shared data.

6.1.1. Common problems with shared state

Most problems with multithreaded programs happen because changes to shared data aren’t correctly protected. For the purposes of this chapter, we’ll summarize the issues as follows.

Lost or buried updates

Lost updates occur when two threads update the same data one after the other. The update made by the first thread is lost because the second one overwrites it. A classic example is two threads incrementing a counter, the current value of which is 10. Because execution of threads is interleaved, both threads can do a read on the counter and think the value is 10, and then both increment it to 11. The problem is that the final value should have been 12, and the update done by one of the threads was lost.

Dirty and unrepeatable reads

A dirty read happens when a thread reads data that another thread is in the process of updating. Before the one thread could completely update the data, the other thread has read inconsistent (dirty) data. Similarly, an unrepeatable read happens when a thread reads a particular data set, but because other threads are updating it, the thread can never do another read that results in it seeing the same data again.

Phantom reads

A phantom read happens when a thread reads data that’s been deleted (or more data is added). The reading thread is said to have performed a phantom read because it has summarily read data that no longer exists.

Brian Goetz’s book Java Concurrency in Practice (Addison-Wesley Professional, 2006) does an incredible job of throwing light on these issues. The book uses Java to illustrate examples, so it isn’t directly useful, but it’s still highly recommended.

6.1.2. Traditional solution

The most obvious solution to these problems is to impose a level of control on those parts of the code that deal with such mutable, shared data. This is done using locks, which are constructs that control the execution of sections of code, ensuring that only a single thread runs a lock-protected section of code at a time. When using locks, a thread can execute a destructive method (one that mutates data) that’s protected with a lock only if it’s able to first obtain an associated lock. If a thread tries to execute such code while some other thread holds the lock, it blocks until the lock becomes available again. The blocking thread is allowed to resume execution only after it obtains the lock at a later time.

This approach might seem reasonable, but it gets complicated the moment more than one piece of mutable data needs a coordinated change. When this happens, each thread that needs to make the change must obtain multiple locks, leading to more contention and resulting in concurrency problems. It’s difficult to ensure correctness of multithreaded programs that have to deal with multiple mutating data structures. Further, finding and fixing bugs in such programs is difficult thanks to the inherently nondeterministic nature of multithreaded programs.

Still, programs of significant complexity have been written using locks. It takes a lot more time and money to ensure things work as expected and a larger maintenance budget to ensure things continue to work properly while changes are being made to the program. It makes you wonder if there isn’t a better approach to solving this problem.

This chapter is about such an approach. Before we get into the meat of the solution, we’ll examine a couple of things. First, we’ll look at the general disadvantages of using locks in multithreaded programs. Then, we’ll take a quick overview of the new issues that arise from the presence of locking.

Disadvantages of locking

The most obvious disadvantage of locking is that code is less multithreaded than it was before the introduction of locks. When one thread obtains and holds a lock, no other thread can execute that code, causing other threads to wait. This can be wasteful, and it reduces throughput of multithreaded applications.

Further, locks are an excessive solution. Consider the case where a thread only wants to read some piece of mutable data. To ensure that no other thread makes changes while it’s doing its work, the reader thread must lock all concerned mutable data. This causes not only writers to block but other readers too. This is unnecessarily wasteful.

Lastly, another disadvantage of locking is that you, the programmer, must remember to lock, and lock the right things, and in the right order. If someone introduces a bug that involves a forgotten lock, it can be difficult to track down and fix. There are no automatic mechanisms to flag this situation and no compile-time or runtime warnings associated with such situations, other than the fact that the program behaves in an unexpected manner! The knowledge of what to lock and in what order to lock things (so that the locks can be released in the reverse order) can’t be expressed within program code—typically, it’s recorded in technical documentation. Everyone in the software industry knows how well documentation works.

Unfortunately, these aren’t the only disadvantages of using locking; it causes new problems too. We’ll examine some of them now.

New problems with locking

When a single thread needs to change more than one piece of mutable data, it needs to obtain locks for all of them. This is the only way for a lock-based solution to ensure coordinated changes to multiple items. The fact that threads need to obtain locks to do their work causes contention for these locks. This contention results in a few issues that are typically categorized as shown in table 6.1.

Table 6.1. Issues that arise from the use of locks

Issue

Description

Deadlock This is the case where two or more threads wait for the other to release locks that they need. This cyclic dependency results in all concerned threads being unable to proceed.
Starvation This happens when a thread isn’t allocated enough resources to do its job, causing it to starve and never complete.
Livelock This is a special case of starvation, and it happens when two threads continue executing (that is, changing their states) but make no progress toward their final goal. Imagine two people meeting in a hallway and each trying to pass the other. If they both wait for the other to move, it results in a deadlock. If they both keep moving toward the other, they still end up blocking each other from passing. This situation results in a livelock, because they’re both doing work and changing states but are still unable to proceed.
Race condition This is a general situation where the interleaving of execution of threads causes an undesired computational result. Such bugs are difficult to debug because race conditions happen in relatively rare scenarios.

With all these disadvantages and issues that accompany the use of locks, you must wonder if there isn’t a better solution to the problem of concurrency and state. We’ll explore this in the next section, beginning with a fresh look at modeling state itself.

6.2. Separating identities and values

Now that we’ve explored the landscape of some of the common problems of concurrent programs and shared state, including the popular solution of locks, we’re ready to examine an alternative point of view. Let’s begin by reexamining a construct offered by most popular programming languages to deal with state—that of objects. OO languages like Java, C++, Ruby, and Python offer the notion of classes that contain state and related operations. The idea is to provide the means to encapsulate things to separate responsibility among various abstractions, allowing for cleaner design. This is a noble goal and is probably even achieved once in a while. But most languages have a flaw in this philosophy that causes problems when these same programs need to run as multithreaded applications. And most programs eventually do need multithreading, either because requirements change or to take advantage of multicore CPUs.

The flaw is that these languages conflate the idea of what Rich Hickey calls identity with that of state. Consider a person’s favorite set of movies. As a child, this person’s set might contain films made by Disney and Pixar. As a grownup, the person’s set might contain other movies, such as ones directed by Tim Burton or Robert Zemeckis. The entity represented by favorite-movies changes over time. Or does it?

In reality, there are two different sets of movies. At one point (earlier), favorite-movies referred to the set containing children’s movies; at another point (later), it referred to a different set that contained other movies. What changes over time, therefore, isn’t the set itself but which set the entity favorite-movies refers to. Further, at any given point, a set of movies itself doesn’t change. The timeline demands different sets containing different movies over time, even if some movies appear in more than one set.

To summarize, it’s important to realize that we’re talking about two distinct concepts. The first is that of an identity—someone’s favorite movies. It’s the subject of all the action in the associated program. The second is the sequence of values that this identity assumes over the course of the program. These two ideas give us an interesting definition of state—the value of an identity at a particular point time. This separation is shown in figure 6.1.

Figure 6.1. It’s important to recognize the separation between what we’re talking about (say, favorite movies, which is an identity) and the values of that identity. The identity itself never changes, but it refers to different values over time.

This idea of state is different from what traditional implementations of OO languages provide out of the box. For example, in a language like Java or Ruby, the minute a class is defined with stateful fields and destructive methods (those that change a part of the object), concurrency issues begin to creep into the world and can lead to many of the problems discussed earlier. This approach to state might have worked a few years ago when everything was single threaded, but it doesn’t work anymore.

Now that you understand some of the terms involved, let’s further examine the idea of using a series of immutable values to model the state of an identity.

6.2.1. Immutable values

An immutable object is one that can’t change once it has been created. To simulate change, you’d have to create a whole new object and replace the old one. In the light of our discussion so far, this means that when the identity of favorite-movies is being modeled, it should be defined as a reference to an immutable object (a set, in this case). Over time, the reference would point to different (also immutable) sets. This ought to apply to objects of any kind, not only sets. Several programming languages already offer this mechanism in some of their data types, for instance, numbers and strings. As an example, consider the following assignment:

x = 101

Most languages treat the number 101 as an immutable value. Languages provide no constructs to do the following, for instance:

x.setUnitsDigit(3)
x.setTensDigit(2)

No one expects this to work, and no one expects this to be a way to transform 101 into 123. Instead, you might do the following:

x = 101 + 22

At this point, x points to the value 123, which is a completely new value and is also immutable. Some languages extend this behavior to other data types. For instance, Java strings are also immutable. In programs, the identity represented by x refers to different (immutable) numbers over time. This is similar to the concept of favorite-movies referring to different immutable sets over time.

6.2.2. Objects and time

As you’ve seen, objects (such as x or favorite-movies) don’t have to physically change for programs to handle the fact that something has happened to them. As discussed previously, they can be modeled as references that point to different objects over time. This is the flaw that most OO languages suffer from: they conflate identities (x or favorite-movies) and their values. Most such languages make no distinction between an identity such as favorite-movies and the memory location where the data relating to that identity is stored. A variable kyle, for example, might directly point to the memory location containing the data for an instance of the Person class.

In typical OO languages, when a destructive method (or procedure) executes, it directly alters the contents of the memory where the instance is stored. Note that this doesn’t happen when the same language deals with primitives, such as numbers or strings. The reason no one seems to notice this difference in behavior is that most languages have conditioned programmers to think that composite objects are different from primitives such as strings and numbers. But this isn’t how things need to be, and there is another way. Instead of letting programs have direct access to memory locations via pointers such as favorite-movies and allowing them to change the content of that memory location, programs should have only a special reference to immutable objects. The only thing they should be allowed to change is this special reference itself, by making it point to a completely different, suitably constructed object that’s also immutable. This concept is illustrated in figure 6.2.

Figure 6.2. A reference that points to completely different immutable values over time

This should be the default behavior of all data types, not only select ones like numbers or strings. Custom classes defined by a programmer should also work this way.

Now that we’ve talked about this new approach to objects and mutation over time, let’s see why this might be useful and what might be special about such references to immutable objects.

6.2.3. Immutability and concurrency

It’s worth remembering that the troubles with concurrency happen only when multiple threads attempt to update the same shared data. In the first part of this chapter, we reviewed the common problems that arise when shared data is mutated incorrectly in a multithreaded scenario. The problems with mutation can be classified into two general types:

  • Losing updates (or updating inconsistent data)
  • Reading inconsistent data

If all data is immutable, then we eliminate the second issue. If a thread reads something, it’s guaranteed to never change while it’s being used. The concerned thread can go about its business, doing whatever it needs to with the data—calculating things, displaying information, or using it as input to other things. In the context of the example concerning favorite movies, a thread might read someone’s favorite set of movies at a given point and use it in a report about popular movies. Meanwhile, a second thread might update a person’s favorite movies. In this scenario, because the sets are immutable, the second thread would create a new set of movies, leaving the first thread with valid and consistent (and merely stale) data.

We’ve glossed over some of the technicalities involved in ensuring that this works, and we’ll explore Clojure’s approach in much greater depth in the following sections. In particular, threads should be able to perform repeated reads correctly, even if another thread updated some or all of the data. Assuming things do work this way, the read problem in a multithreaded situation can be considered solved. It leaves only the issue of when two or more threads try to update the same data at the same time.

Solving this second problem requires some form of supervision by the language runtime and is where the special nature of references comes into play. Because no identity has direct access to the contents of various memory locations (which in turn contain the data objects), the language runtime has a chance of doing something to help supervise writes. Specifically, because identities are modeled using special references, as mentioned previously, the language can provide constructs that allow supervised changes to these indirect references. These constructs can have concurrency semantics, thereby making it possible for multiple threads to update shared data correctly. The semantics can ensure more than safe writes; they can signal errors when writes fail or enforce certain other constraints when a write is to be made.

This isn’t possible in most other popular languages today, because they allow direct access to (and mutation of) memory locations. A language that satisfies two requirements can hope to solve the concurrency problem: the first is that identities not point directly to memory locations but do so indirectly via managed references, and the second is that data objects themselves be immutable. The separation of identity and state is the key. You’ll see Clojure’s flavor of this approach over the next few sections.

6.3. Clojure’s way

As you saw in the previous section, there’s an alternative when it comes to modeling identities and their state. Instead of letting an identity be a simple reference (direct access to a memory location and its contents), it can be a managed reference that points to an immutable value. Over the course of the program, this reference can be made to point to other immutable values as required by the program logic. If state is modeled this way, then the programming language facilities that allow a managed reference to point to different things can support concurrency semantics—they can check for modified data, enforce validity, enforce that other programming constructs be used (such as transactions), and so forth. This is exactly the Clojure way.

Clojure provides managed references to state, as described previously. It provides four different kinds of managed references, each suitable in different situations. It also provides language-level constructs that help in changing what these references point to. Further, to coordinate changes to more than one reference, Clojure exposes an interesting take on a software transactional memory (STM) system. We’ll examine each of these in detail right after we talk about performance.

6.3.1. Immutability and performance

For any language to work this way (managed references, immutable objects), an important requirement must be met—that of performance. Working with this model of state and mutation needs to be as fast as the old way of in-place mutation. Traditional solutions to this issue have been unsatisfactory, but Clojure solves it in an elegant way.

Immutability by copying

Let’s again consider the example concerning movies and multiple threads. Imagine that the first thread is dealing with Rob’s set of favorite movies when he was a child. If a second thread were to update his favorites to a new set, the data seen by the first thread should still be valid. One way to achieve this is to make a copy of the object being updated so that readers still have valid (if old) data while the writer updates it to the new object.

The problem with this approach is that naïvely copying something over in this manner is extremely inefficient. Often, the speed of such a copy operation grows linearly with the size of the objects being copied. If every write involved such an expensive operation, it would be impossible to use in a production environment. Therefore, given that an approach involving blind copying of data isn’t viable, the alternative must involve sharing the data structures in question. Specifically, the new and updated objects must in some way point to the old values while making additional changes required to perform updates.

To make the performance requirements clearer, such an implementation must have approximately the same performance characteristics as the old mutable implementation. For example, a hash table must behave in a constant time (or near enough) manner. This performance guarantee must be satisfied, in addition to satisfying the previous constraint that the older version still be usable. This would allow other threads that had read the data prior to the update to continue with their job. In summary, the requirements are that the immutable structures do the following:

  • Leave the old version of itself in a usable state when it mutates
  • Satisfy the same performance characteristics as the mutable versions of themselves

You’ll now see how Clojure satisfies these requirements.

Persistent data structures

The common use of the term persistence in computer science refers to persisting data into a nonvolatile storage system, such as a database. But there’s another way that term is used, one that’s quite common in the functional programming space. A persistent data structure is one that preserves the previous version of itself when it’s modified. Older versions of such data structures persist after updates. Such data structures are inherently immutable, because update operations yield new values every time.

All of the core data structures offered by Clojure are persistent. These include maps, vectors, lists, and sets. These persistent data structures also perform extremely well because instead of using copying, they share structure when an update needs to be done. Specifically, they maintain nearly all the performance guarantees that are made by such data structures, and their performance is on par with or extremely close to that of similar data structures that are provided by the Java language.

With this implementation, Clojure has the means to provide the managed reference model for mutating state. We’ll examine this in the next section.

6.3.2. Managed references

Given Clojure’s efficient implementation of persistent data structures, the approach of modeling state through managed references becomes viable. Clojure has four distinct offerings in this area, each useful in certain scenarios. Table 6.2 gives an overview of the options available, and we’ll discuss each of these types in this order in the following sections.

Table 6.2. Clojure provides four different types of managed references.

Managed reference type

Useful for

ref Shared, synchronous, coordinated changes
agent Shared, asynchronous, uncoordinated changes
atom Shared, synchronous, uncoordinated changes
var Isolated changes

Clojure provides a managed reference for the different situations that arise when writing programs that use multiple threads. This ranges from the case that needs to isolate any change to within the thread making it, to the case when threads need to coordinate changes that involve multiple shared data structures. In the next few sections, we’ll examine each one in turn.

In the first section of this chapter, we examined the problems faced by multithreaded programs when shared data is involved. These problems are typically handled using locks, and we also examined the problems associated with locks.

Managed references and language-level support for concurrency semantics offer an alternative to locks. In the next section, we’ll examine the first of Clojure’s managed references—the ref—and show how the language provides lock-free concurrency support.

6.4. Refs

Clojure provides a special construct in ref (short for reference) to create a managed reference that allows for synchronous and coordinated changes to mutable data. A ref holds a value that can be changed in a synchronous and coordinated manner. As an example, let’s consider an expense-tracking domain.

6.4.1. Creating refs

First, you’ll want to create a ref to hold all the users of your imaginary system. The following is an example of this, and the ref is initialized with an empty map:

(def all-users (ref {}))

At this point, all-users is a ref that points to the initial value of an empty map. You can check this by dereferencing it using the deref function, which returns the current value from within the ref:

(deref all-users)
;=> {}

Clojure also provides a convenient reader macro to dereference such a managed reference: the @ character. The following works the same way as calling deref:

@all-users
;=> {}

By the way, if you just ask for the value of the all-users ref, here’s what you might see:

all-users
;=> #<Ref@227e9896: {}>

This is the ref itself, so you always have to remember to dereference it when you want the underlying value. Now that you know how to create and read back a ref, you’re ready to see how you can go about changing what it points to.

6.4.2. Mutating refs

Now, you’ll write a function that adds a new user to your existing set. Clojure’s refs can be changed using the ref-set, alter, or commute functions.

ref-set

ref-set is the most basic of these functions; it accepts a ref and a new value and replaces the old value with the new. Try the following to see it in action:

(ref-set all-users {})
IllegalStateException No transaction running  clojure.lang.LockingTransaction.getEx (LockingTransaction.java:208)

Because refs are meant for situations where multiple threads need to coordinate their changes, the Clojure runtime demands that mutating a ref be done inside an STM transaction. An STM transaction is analogous to a database transaction but for changes to in-memory data structures. We’ll say more about Clojure’s STM system in the following section; for now, you’ll start an STM transaction using a built-in macro called dosync.

You can check that this works by trying your previous call to ref-set but this time inside the scope of a dosync:

(dosync
  (ref-set all-users {}))
;=> {}

That works as expected, and you can use ref-set like this to reset your list of users. dosync is required for any function that mutates a ref, including the other two mentioned earlier, alter and commute.

alter

Typically, a ref is mutated by taking its current value, applying a function to it, and storing the new value back into it. This read-process-write operation is a common scenario, and Clojure provides the alter function that can do this as an atomic operation. The general form of this function is

(alter ref function & args)

The first and second arguments to alter are the ref that’s to be mutated and the function that will be applied to get the new value of the ref. When the function is called, the first argument will be the current value of the ref, and the remaining arguments will be the ones specified in the call to alter (args).

Before examining the commute function, let’s get back to the intention of writing a function to add a new user to your list of existing users. First, here’s a function to create a new user:

(defn new-user [id login monthly-budget]
  {:id id
   :login login
   :monthly-budget monthly-budget
   :total-expenses 0})

This uses a Clojure map to represent a user—a common pattern used where traditional objects are needed. We’ve deliberately kept the representation simple; in real life your users would probably be a lot more, well, real. Next, here’s the add-user function:

(defn add-new-user [login budget-amount]
  (dosync
    (let [current-number (count @all-users)
          user (new-user (inc current-number) login budget-amount)]
      (alter all-users assoc login user))))

Note the use of dosync. As mentioned previously, it starts an STM transaction, which allows you to use alter. In the preceding code snippet, alter is passed the all-users ref, which is the one being mutated. The function you pass it is assoc, which takes a map, a key, and a value as parameters. It returns a new map with that value associated with the supplied key. In this case, your newly created user gets associated with the login name. Note that while the first argument to alter is the all-users ref itself, assoc will receive the current value of the ref all-users.

Further note that the code includes the entire let form inside the transaction started by dosync. The alternative would have been to call only alter inside the dosync. Clojure wouldn’t have complained because dereferencing a ref (@all-users) doesn’t need to happen inside a transaction. You do this to ensure that you see a consistent set of users. You want to avoid the buried update problem where two threads read the count and one thread commits a new user (increasing the real count), causing the other thread to commit a new user with a duplicate ID. Here’s the functionality in action:

(add-new-user "amit" 1000000)
;=> {"amit" {:id 2, :login "amit", :monthly-budget 1000000, :total-expenses 0}}

Notice that alter returns the final state of the ref. If you now call it again, you’ll see the following:

(add-new-user "deepthi" 2000000)
;=> {"deepthi" {:id 3, :login "deepthi", :monthly-budget 2000000,
                :total-expenses 0},
     "amit" {:id 2, :login "amit", :monthly-budget 1000000,
             :total-expenses 0}}

As you can see, the ref is mutating as expected. One final note: except for the :id value it doesn’t matter in what order you add users in the previous example. If two threads were both trying to add a user to your system, you wouldn’t care in what order they’re added. Such an operation is said to be commutative, and Clojure has optimized support for commutes.

commute

When two threads try to mutate a ref using either ref-set or alter, and one of them succeeds (causing the other to fail), the second transaction starts over with the latest value of the ref in question. This ensures that a transaction doesn’t commit with inconsistent values. The effect of this mechanism is that a transaction may be tried multiple times.

For those situations where it doesn’t matter what the most recent value of a ref is (only that it’s consistent and recent), Clojure provides the commute function. The name derives from the commutative property of functions, and you might remember this from your high school math classes. A function is commutative if it doesn’t matter in which order the arguments are applied. For example, addition is commutative, whereas subtraction isn’t:

a + b = b + a
a – b != b - a

The commute function is useful where the order of the function application isn’t important. For instance, imagine that a number was being incremented inside a transaction. If two threads were to go at it in parallel, at the end of the two transactions, it wouldn’t matter which thread had committed first. The result would be that the number was incremented twice.

When the alter function is applied, it checks to see if the value of the ref has changed because of another committed transaction. This causes the current transaction to fail and for it to be retried. The commute function doesn’t behave this way; instead, execution proceeds forward and all calls to commute are handled at the end of the transaction. The general form of commute is similar to alter:

(commute ref function & args)

As explained earlier, the function passed to commute should be commutative. Similar to alter, the commute function also performs the read-apply-write operation on one atomic swoop.

You’ve now seen the three ways in which a ref can be mutated. In showing these, we’ve mentioned STM transactions quite a bit. In the next section, you’ll learn a little more about Clojure’s implementation of the STM system.

6.4.3. Software transactional memory

A common solution to the problems of shared data and multithreading is the (careful) use of locks. But this approach suffers from several problems, as we already discussed. These issues make using locks messy and error prone while also making code based on locks infamously difficult to debug.

STM is a concurrency control mechanism that works in a fashion similar to database transactions. Instead of controlling access to data stored on disks, inside tables and rows, STMs control access to shared memory. Using an STM system offers many advantages to multithreaded programs, the most obvious being that it’s a lock-free solution. You can think of it as getting all the benefits of using locks but without any of the problems. You also gain increased concurrency because this is an optimistic approach compared with the inherently pessimistic approach of locking.

In this section, you’ll get a high-level overview of what STM is and how it works.

STM transactions

Lock-based solutions prevent more than one thread from executing a protected part of the code. Only the thread that acquired the right set of locks is allowed to execute code that has been demarcated for use with those locks. All other threads that want to execute that same code block it until the first thread completes and relinquishes those locks.

An STM system takes a nearly opposite approach. First, code that needs to mutate data is put inside a transaction. In the case of Clojure, this means using the dosync macro. Once this is done, the language runtime takes an optimistic approach in letting threads execute the transaction. Any number of threads are allowed to begin the transaction. Changes made to refs within the transaction are isolated, and only the threads that made the changes can see the changed values.

The first thread that completely executes the block of code comprising the transaction is allowed to commit the changed values. Once a thread commits, when any other thread attempts to commit, that transaction is aborted and the changes are rolled back.

The commit performed when a transaction is successful is atomic in nature. This means that even if a transaction makes changes to multiple refs, as far as the outside world is concerned, they all appear to happen at the same instant (when the transaction commits). STM systems can also choose to retry failed transactions, and many do so until the transaction succeeds. Clojure also supports this automatic retrying of failed transactions, up to an internal limit.

Now that you know how transactions work at a high level, let’s recap an important set of properties that the STM system exhibits.

Atomic, consistent, isolated

The Clojure STM system has ACI properties (atomicity, consistency, isolation). It doesn’t support durability because it isn’t a persistent system and is based on volatile, in-memory data. To be specific, if a transaction mutates several refs, the changes become visible to the outside world at one instant. Either all the changes happen, or, if the transaction fails, the changes are rolled back and no change happens. This is how the system supports atomicity.

When refs are mutated inside a transaction, the changed data is called in-transaction values. This is because they’re visible only to the thread that made the changes inside the transaction. In this manner, transactions isolate the changes within themselves (until they commit).

If any of the refs are changed during the course of a transaction, the entire transaction is retried. In this manner, the STM system supports consistency. For extra protection, Clojure’s refs (and also agents and atoms) accept validator functions when created. These functions are used to check the consistency of the data when changes are made to it. If the validator function fails, the transaction is rolled back.

Before moving onto the other types of managed references in Clojure, we’ll make one final point about the STM.

MVCC

Clojure’s STM system implements multiversion concurrency control (MVCC). This is the type of concurrency supported by several database systems such as Oracle and PostgreSQL. In an MVCC system, each contender (threads in the case of Clojure) is given a snapshot of the mutable world when it starts its transaction.

Any changes made to the snapshot are invisible to other contenders until the changes are committed at the end of a successful transaction. But thanks to the snapshot model, readers never block writers (or other readers), increasing the inherent concurrency that the system can support. In fact, writers never block readers either, thanks to the same isolation. Contrast this with the old locking model where both readers and writers block while one thread does its job.

Having seen the way managed references work in Clojure and also how the associated mechanism of the STM works, you can write multithreaded programs that need to coordinate changes to shared data. In the next section, we’ll examine a method to mutate data in an uncoordinated way.

6.5. Agents

Clojure provides a special construct called an agent that allows for asynchronous and independent changes to shared mutable data. For instance, you may want to time the CPU while it executes some code of interest. In this section, you’ll see how to create, mutate, and read agents. The agent function allows the creation of agents, which hold values that can be changed using special functions. Clojure provides two functions, send and send-off, that result in mutating the value of an agent. Both accept the agent that needs to be updated, along with a function that will be used to compute the new value. The application of the function happens at a later time, on a separate thread. By corollary, an agent is also useful to run a task (function) on a different thread, with the return value of the function becoming the new value of the agent. The functions sent to agents are called actions.

6.5.1. Creating agents

Creating an agent is similar to creating a ref. As mentioned, the agent function allows you to create an agent:

(def total-cpu-time (agent 0))

Dereferencing an agent to get at its current value is similar to using a ref:

(deref total-cpu-time)
;=> 0

Clojure also supports the @ reader macro to dereference agents, so the following is equivalent to calling deref:

@total-cpu-time
;=> 0

Having created an agent, let’s see how you can mutate it.

6.5.2. Mutating agents

As described in the preceding paragraphs, agents are useful when changes to a particular state need to be made in an asynchronous fashion. The changes are made by sending an action (a regular Clojure function) to the agent, which runs on a separate thread at a later time. There are two flavors of this—send and send-off—and we’ll examine them both.

send

The general form of the send function is as follows:

(send the-agent the-function & more-args)

As an example, consider adding a few hundred milliseconds to the total-cpu-time agent you created earlier:

(send total-cpu-time + 700)

The addition operator in Clojure is implemented as a function, no different from regular functions. The action function sent to an agent should accept one or more parameters. When it runs, the first parameter it’s supplied is the current value of the agent, and the remaining parameters are the ones passed via send.

In this example, the + function is sent to the total-cpu-time agent, and it uses the current value of the agent (which is 0) as the first argument and 700 as the second argument. At some point in the future, although it isn’t noticeable in the example because it happens almost immediately, the + function executes and the new value of total-cpu-time will be set as the value of the agent. You can check the current value of the agent by dereferencing it:

(deref total-cpu-time)
;=> 700

If the action takes a long time to run, it may be a while before dereferencing the agent shows the new value. Dereferencing the agent before the agent runs will continue to return the old value. The call to send itself is nonblocking, and it returns immediately.

Actions sent to agents using send are executed on a fixed thread pool maintained by Clojure. If you send lots of actions to agents (more than the number of free threads in this pool), they get queued and will run in the order in which they were sent. Only one action runs on a particular agent at a time. This thread pool doesn’t grow in size, no matter how many actions are queued up. This is depicted in figure 6.3. This is why you should use send for actions that are CPU intensive and don’t block, because blocking actions will use up the thread pool. For blocking actions, Clojure provides another function—send-off—and we’ll look at that now.

Figure 6.3. The thread pool used for the send function is fixed based on the number of cores available. If all the threads are busy, then functions get queued.

send-off

The function send-off can handle potential blocking actions. The general form of the send-off function is exactly the same as for send:

(send-off the-agent the-function & more-args)

The semantics of what happens when send-off is called are the same as that of send, the only difference being that it uses a different thread pool from the one used by send, and this thread pool can grow in size to accommodate more actions sent using send-off. Again, only one action runs on a particular agent at a time.

We’ll now look at a few convenient constructs provided by Clojure that are useful when programming using agents.

6.5.3. Working with agents

This section will examine a few functions that come in handy when working with agents. First, a common scenario when using agents to do work asynchronously is that several actions are sent (using either send or send-off), and then one waits until they all complete. Clojure provides two functions that help in this situation: await and await-for.

Next, we’ll look at ways to test agents for errors. After all, you can send any function to an agent, which means you can send any arbitrary code to the agent thread pools. If your code throws an error, there needs to be a way to examine what went wrong.

And finally, another common use case is that a notification is desired when an action sent to an agent completes successfully. This is where watchers come in. You’ll see how the value of an agent can be kept consistent by validating it with some business rules each time an attempt is made to change it.

await and await-for

await is a function that’s useful when execution must stop and wait for actions that were previously dispatched to certain agents to be completed. The general form is

(await & the-agents)

As an example, let’s say you had agents named agent-one, agent-two, and agent-three. Let’s also say you sent several actions to these three agents, either from your own thread, other threads, or from another agent. At some point, you could cause the current thread to block until all actions sent to your three agents completed, by doing the following:

(await agent-one agent-two agent-three)

await blocks indefinitely, so if any of the actions didn’t return successfully, the current thread wouldn’t be able to proceed. To avoid this, Clojure also provides the await-for function. The general form looks similar to that of await, but it accepts a maximum timeout in milliseconds:

(await-for timeout-in-millis & the-agents)

Using await-for is safer in the sense that the max wait time can be controlled. If the timeout does occur, await-for returns nil. Here’s an example:

(await-for 1000 agent-one agent-two agent-three)

This will abort the blocking state of the thread if the timer expires before the actions have completed. It’s common to check if the actions succeeded or not by testing the agents for any errors after using await-for.

Agent errors

When an action doesn’t complete successfully (it throws an exception), the agent knows about it. If you try to dereference an agent that’s in such an error state, it will return the previous successful result. Take a look:

(def bad-agent (agent 10))

This sets up an agent with an initial value of 10. You’ll now send it an action that will cause an error, leaving the agent in an error state:

(send bad-agent / 0)
;=> #<Agent@125b9ec1 FAILED: 10>

Notice the agent is in the FAILED state, with an unchanged value of 10 (caused by the intentional divide-by-zero error). You can now try to dereference bad-agent:

(deref bad-agent)
;=> 10

Further, if you now try to send it another action, even if it might have succeeded, the agent will complain about its error state, for instance:

(send bad-agent / 2)
ArithmeticException Divide by zero  clojure.lang.Numbers.divide (Numbers.java:156)

You can always programmatically discern what the error is by using the agent-error function:

(agent-error bad-agent)
;=> #<ArithmeticException java.lang.ArithmeticException: Divide by zero>

agent-error returns the exception thrown during the agent thread’s execution. The error object returned is an instance of the particular exception class corresponding to the error that happened and can be queried using Java methods, for example:

(let [e (agent-error bad-agent)
      st (.getStackTrace e)]
  (println (.getMessage e))
     (println (clojure.string/join "
" st)))

As mentioned, if an agent has an error, you can’t send it any more actions. If you do, Clojure throws the same exception, informing you of the current error. To make the agent usable again, Clojure provides the clear-agent-errors function:

(clear-agent-errors bad-agent)

The agent is now ready to accept more actions.

Validations

You’ve seen how to create new agents. You can create them with more options as well. Here’s the complete general form of the agent function that creates new agents:

(agent initial-state & options)

The options allowed are

:meta metadata-map
:validator validator-fn

If the :meta option is used, then the map supplied with it will become the metadata of the agent. If the :validator option is used, it should be accompanied by either nil or a function that accepts one argument. The validator-fn is passed the intended new state of the agent, and it can apply any business rules to allow or disallow the change to occur. If the validator function returns false or throws an exception, then the state of the agent isn’t mutated.

You’ve now seen how agents can be used in Clojure. Before moving on to the next kind of managed reference, you’ll see how agents can also be used to cause side effects from inside STM transactions.

6.5.4. Side effects in STM transactions

We said earlier that Clojure’s STM system automatically retries failed transactions. After the first transaction commits, all other transactions that had started concurrently will abort when they, in turn, try to commit. Aborted transactions are then started over. This implies that code inside a dosync block can potentially execute multiple times before succeeding, and for this reason, such code shouldn’t contain side effects. If you do, you can expect the side effect to also occur multiple times. There’s no way to alter this behavior, so you have to be careful that you don’t do this.

As an example, if there was a call to println inside a transaction, and the transaction was tried several times, the println will be executed multiple times. This behavior would probably not be desirable.

There are times when a transaction does need to generate a side effect. It could be logging or anything else, such as writing to a database or sending a message on a queue. Agents can be used to facilitate such intended side effects. Consider the following pseudo-code:

(dosync
  (send agent-one log-message args-one)
  (send-off agent-two send-message-on-queue args-two)
  (alter a-ref ref-function)
    (some-pure-function args-three))

Clojure’s STM system holds all actions that need to be sent to agents until transactions succeed. In the pseudo-code shown here, log-message and send-message-on-queue are actions that will be sent only when the transaction succeeds. This ensures that even if the transaction is tried multiple times, the side effect causing actions gets sent only once. This is the recommended way to produce side effects from within a transaction.

This section walked through the various aspects of using agents. You saw that agents allow asynchronous and independent changes to mutable data. The next kind of managed reference is called an atom, which allows for synchronous and independent changes to mutable data.

6.6. Atoms

Clojure provides a special construct in an atom that allows for synchronous and independent changes to mutable data. The difference between an atom and an agent is that updates to agents happen asynchronously at some point in the future, whereas atoms are updated synchronously (immediately). Atoms differ from refs in that changes to atoms are independent from each other and can’t be coordinated, so they either all happen or none do.

6.6.1. Creating atoms

Creating an atom looks similar to creating either refs or agents:

(def total-rows (atom 0))

total-rows is an atom that starts out being initialized to zero. You could use it to hold the number of database rows inserted by a Clojure program as it restores data from a backup, for example. Reading the current value of the atom uses the same dereferencing mechanism used by refs and agents:

(deref total-rows)
;=> 0

Or it uses the @ reader macro:

@total-rows
;=> 0

Now that you’ve seen how to create atoms and read their values, let’s address mutating them.

6.6.2. Mutating atoms

Clojure provides several ways to update the value of an atom. There’s an important difference between atoms and refs, in that changes to one atom are independent of changes to other atoms. Therefore, there’s no need to use transactions when attempting to update atoms.

reset!

The reset! function doesn’t use the existing value of the atom and simply sets the provided value as the new value of the atom. The general form of the function is

(reset! atom new-value)

This might remind you of the ref-set function, which also does the same job but for refs.

swap!

The swap! function has the following general form:

(swap! the-atom the-function & more-args)

You could pass swap! the addition function whenever you finish inserting a batch of rows:

(swap! total-rows + 100)

Here, in a synchronous manner, the + function is applied to the current value of total-rows (which is zero) and 100. The new value of total-rows becomes 100. If you were to use a mutation function that didn’t complete before another thread changed the value of the atom, swap! would then retry the operation until it did succeed. For this reason, mutation functions should be free of side effects.

Clojure also provides a lower-level function called compare-and-set! that can be used to mutate the value of an atom. swap! internally uses compare-and-set!.

compare-and-set!

Here’s the general form of the compare-and-set! function:

(compare-and-set! the-atom old-value new-value)

This function atomically sets the value of the atom to the new value, if the current value of the atom is equal to the supplied old value. If the operation succeeds, it returns true; otherwise it returns false. A typical workflow of using this function is to dereference the atom in the beginning, do something with the value of the atom, and then use compare-and-set! to change the value to a new one. If another thread had changed the value in the meantime (after it had been dereferenced), then the mutation would fail.

The swap! function does that internally: it dereferences the value of the atom, applies the provided mutation function, and attempts to update the value of the atom using compare-and-set! by using the value that was previously dereferenced. If compare-and-set! returns false (the mutation failed because the atom was updated elsewhere), the swap! function reapplies the mutation function until it succeeds.

Atoms can be used whenever there’s a need for some state but not for coordination with any other state. Using refs, agents, and atoms, all situations that demand mutation of shared data can be handled. Our last stop will be to study vars, because they’re useful when state needs to be modified but not shared.

6.7. Vars

We introduced vars in chapter 3. In this section, we’ll take a look at how vars can be used to manage state in an isolated (thread-local) manner.

6.7.1. Creating vars and root bindings

Vars can be thought of as pointers to mutable storage locations, which can be updated on a per-thread basis. When a var is created, it can be given an initial value, which is referred to as its root binding:

(def hbase-master "localhost")

In this example, hbase-master is a var that has a binding of "localhost". Here, it acts as a constant. You could also use it as a special var, by declaring it as follows:

(def  ^:dynamic *hbase-master* "localhost")

The starting and ending asterisks are conventions that denote that this var ought to be rebound before use. (If you name a var like this but without ^:dynamic, Clojure will emit a warning.) In normal use dynamic vars are just like normal vars:

(def ^:dynamic *hbase-master* "localhost")
;=> #'user/*hbase-master*
(println "Hbase-master is:" *hbase-master*)
Hbase-master is: localhost
;=> nil

Also just like normal vars, if you attempt to use a dynamic var without a root binding you’ll get a special Unbound object:[2]

2

In earlier versions of Clojure, using an unbound var would throw an exception.

Now that you know how to make a dynamic var, we’ll review how to rebind its value.

6.7.2. Var bindings

Whether a var has a root binding or not, when the binding form is used to update the var, that mutation is visible only to that thread. If there was no root binding, other threads would see no root binding; if there was a root binding, other threads would continue to see that value. Let’s look at an example. You’ll create a function that will fetch the number of rows in a Users table from different databases: test, development, and staging. Imagine that you define the database host using a var like so:

(def ^:dynamic *mysql-host*)

This var has no root binding, so it will need to be bound before use. You’ll do that in a function that’s meant to do a database query, but for the purposes of this example it will return some dummy data such as the length of the hostname. In the real world, you’d run the query against the database using something like a JDBC library:

(defn db-query [db]
  (binding [*mysql-host* db]
    (count *mysql-host*)))

Next, you’ll create a list of the hosts you want to run your function against:

(def mysql-hosts ["test-mysql" "dev-mysql" "staging-mysql"])

Finally, you could run your query function against all the hosts:

(pmap db-query mysql-hosts)
;=> (10 9 13)

pmap works like map, but each time the supplied function is called on an element of the list, it’s done so on a different available thread from an internally maintained thread pool. The call to binding sets up *mysql-host* to point to a different host, and the query function proceeds appropriately. Each execution of the db-query function sees a different value of *myql-host*, as expected.

We’ve covered the four different options that Clojure offers when it comes to concurrency, state, and performing updates—refs, agents, atoms, and vars—and some different scenarios in which each would be useful. You’ll eventually run into a situation where depending on your situation, one of these is a good fit, and you’ll be grateful for Clojure’s language-level support for lock-free concurrency.

6.8. State and its unified access model

This section is a quick recap of the constructs Clojure offers for managing state. We covered each of them over the past few sections, and it’s now possible to make an observation. All of the constructs for managing state enjoy a unified access model that allows you to manage them similarly. This is true whether the managed reference is a ref, an agent, or an atom. Let’s take another quick look at these functions.

6.8.1. Creating

Here are the functions that can create each type of managed reference:

(def a-ref (ref 0))
(def an-agent (agent 0))
(def an-atom (atom 0))

Notice how each accepts an initial value during creation.

6.8.2. Reading

All three kinds of references can be dereferenced the same way:

(deref a-ref) or @a-ref
(deref an-agent) or @an-agent
(deref an-atom) or @an-atom

This uniformity makes Clojure’s references easier to use, because they work in such a similar manner. Let’s also recap how their values can be changed.

6.8.3. Mutation

Changing a managed reference in Clojure always follows the same model: a function is applied to the current value, and the return value is set as the new value of the reference. Table 6.3 shows the functions that allow such mutation.

Table 6.3. Ways to mutate refs, agents, and atoms

Refs

Agents

Atoms

(ref-set ref new-value) (alter ref function & args) (commute ref function & args) (send agent function & args) (send-off agent function & args) (reset! atom new-value) (swap! atom function & args) (compare-and-set! atom old-value new-value)

While we’re on the topic of mutation, it’s worthwhile to note that Clojure provides a hook, which can be used to run arbitrary code when a reference changes state. This mechanism works for refs, agents, atoms, and vars.

6.8.4. Transactions

Finally, there’s the question of which references need transactions and which don’t. Because refs support coordinated changes, mutating them needs the protection of STM transactions: all such code needs to be inside the dosync macro. Agents and atoms don’t need STM transactions. Functions used to calculate new values of refs or atoms must be free of side effects, because they could be retried several times.

6.8.5. Watching for mutation

Sometimes it’s useful to add an event listener that gets notified when the value of a stateful construct changes. Clojure provides the add-watch function for this purpose.

add-watch

The add-watch function allows you to register a regular Clojure function as a “watcher” against any kind of reference. When the value of the reference changes, the watcher function is run.

The watcher must be a function of four arguments:

  • A key to identify the watcher (the-key)
  • The reference it’s being registered against (the-ref)
  • The old value of the reference (old-value)
  • The new value of the reference (new-value)

The add-watch function itself accepts three arguments: the reference to watch, a key to identify the watch you’re adding, and the watcher function. You can use the key argument to remove the watch later. Here it is in action:

(def adi (atom 0))
(defn on-change [the-key the-ref old-value new-value]
  (println "Hey, seeing change from" old-value "to" new-value))
(add-watch adi :adi-watcher on-change)

Now that it’s all set up, you can test it. You’ll check the current value of adi and then update it:

@adi
;=> 0
(swap! adi inc)
Hey, seeing change from 0 to 1
;=> 1

As mentioned before, this can be used for all of Clojure’s special-managed references.

remove-watch

It’s also possible to remove a watch if it’s no longer required. Clojure provides the remove-watch function to do this. Using it is simple: call remove-watch with the reference to stop watching and the key you used in the add-watch call. The following example removes the watch you added earlier:

(remove-watch adi :adi-watcher)

6.9. Deciding which reference type to use

We have now covered all four options Clojure gives you for managing state mutation. You may be a little dizzy with choices and unsure which to use for any given problem. Here’s a guide to your decision making.

The most basic reference type is the var. It’s only useful for isolating changes (either to a particular thread or a particular scope of code), not coordinating or sharing those changes. When you have an ordinarily global variable (such as a database connection or configuration map) but just need to change it to another value for a particular run of code, use a dynamic var with binding to rebind its value. But vars can’t be written to by multiple parts of your code.

Atoms are one step more powerful than vars: they’re the simplest way to manage a state change that must be written to and read by multiple threads. The vast majority of the time you’ll be using atoms because you don’t need anything more. But they have two drawbacks. First, multiple atoms cannot be changed together in an atomic and coordinated way. Second, changes to an atom must be free of side effects because its swap! function may be run multiple times. If you need coordination or side effects, you need to use one of the remaining two options.

Refs are atoms with coordination. Instead of using a giant atom, you can split up your state into multiple refs and read and write to several of them atomically inside a dosync transaction. If you have multiple pieces of shared state that must be updated together but rarely all of them in a single transaction, you can reduce the amount of contention in your application and increase concurrency. (This is similar to the tradeoff you’d make when deciding to use a single big lock or multiple smaller locks.) Refs add coordination to atoms, but like atoms all changes to them inside a transaction must be free of side effects because the transaction may be retried.

Agents are the only reference type that can tolerate side effects, but the cost for this ability is more complex management. Because side-effecting operations cannot be safely retried, agents have error states that must be checked and cleared if their operation fails. Additionally, the side-effecting action runs asynchronously: you must explicitly send a mutation function to run and wait a possibly indefinite amount of time for it to complete in another thread. But agents can be easily combined with refs inside dosync if you have a mostly pure mutation (using refs) with just a little bit of side effect that must be done only when the transaction is successful (using an agent).

We’ve completed our examination of Clojure’s reference types. In the next section we’ll look at another mechanism Clojure provides just to increase concurrency.

6.10. Futures and promises

A future is an object that represents the result of a function that will execute on a different thread. A promise is an object that represents a value that will be delivered to it at some point in the future. Clojure provides futures and promises as easy ways to increase concurrency in your program. But they aren’t really for state management because unlike reference types they can only ever have one value. What distinguishes futures and promises from ordinary values is that their value may not yet be known.

We’ll explore the use of futures first.

6.10.1. Futures

A future is a simple way to run code on a different thread, and it’s useful for long-running computations or blocking calls that can benefit from multithreading. To understand how to use it, examine this contrived function that takes more than 5 seconds to run:

(defn long-calculation [num1 num2]
  (Thread/sleep 5000)
  (* num1 num2))

Now that you have this slow-running function, let’s imagine you needed to run multiple such computations. The code might look like the following:

(defn long-run []
  (let [x (long-calculation 11 13)
        y (long-calculation 13 17)
        z (long-calculation 17 19)]
    (* x y z)))

If you run this in the read-evaluate-print loop (REPL) and use time to see how long this takes, you might see something like this:

(time (long-run))
"Elapsed time: 14998.165 msecs"
;=> 10207769

Now, you can see the long-run will benefit from being multithreaded. That’s where futures come in. The general form of a future is

(future & body)

It returns an object that will invoke body on a separate thread.[3] The returned object can be dereferenced for the value of body. The deref asking for the value will block until the result is available. The result of the computation is cached, so subsequent queries for the value are immediate. Now you’ll write a faster version of the long-run function:

3

You may remember that vars can have different values in different threads and different dynamic scopes. The code run in a future or an agent will always see the vars as having the value of the context that created the future or sent to an agent at the moment future or send/send-off is called. Thus you can create a future or call send inside a binding and rest easy that the value of the binding will be the same inside the future or agent’s thread.

You need to test this using the time function as well. If none of the futures can run in parallel (such as on a single-core machine) this may still take 15 seconds, but if you have at least four cores on your machine you may find that the entire operation completes in 5 seconds:

(time (fast-run))
"Elapsed time: 5000.078 msecs"
;=> 10207769

As you can see, futures are a painless way to get things to run on a different thread. Here are a few future-related functions Clojure provides:

  • future?—Checks to see if the object is a future, and returns true if it is.
  • future-done?—Returns true if the computation represented by this future object is completed.
  • future-cancel—Attempts to cancel this future. If it has already started executing, it doesn’t do anything.
  • future-cancelled?—Returns true if the future has been cancelled.

So you can use futures whenever you need something to run asynchronously on a different thread. As you’ve seen, Clojure makes this very straightforward. Let’s cover promises next.

6.10.2. Promises

A promise is an object that represents a commitment that a value will be delivered to it. You create one using the no-argument promise function:

(def p (promise))

To ask for the promised value, you can dereference it:

(def value (deref p))

Or, as usual, you can use the reader macro version of dereferencing:

@p
Warning

Don’t dereference the preceding promise at the REPL; it will block and you’ll have no way to unblock it!

The way the value delivery system works is via the use of the deliver function. The general form of this function is

(deliver promise value)

Typically, this function is called from a different thread, so it’s a great way to communicate between threads. The deref function (or the reader macro version of it) will block the calling thread if no value has been delivered to it yet. The thread automatically unblocks when the value becomes available. Here’s an example of delivering a value to a promise via a future:

Together, futures and promises are ways to write concurrent programs that need to pass data between threads in a simple way. They’re nice, complementary additions to the various other concurrency semantics you saw earlier in this chapter.

6.11. Summary

We’ve covered some fairly heavy material in this chapter! We began with a look at the new reality of an increasing number of cores inside CPUs and the need for increasingly multithreaded software. We then looked at some of the problems encountered when programs have more than one thread of execution, specifically when these threads need to make changes to shared data. We looked at the traditional way of solving these problems—using locks—and then briefly looked at the new problems that they introduce.

Finally, we looked at Clojure’s approach to these issues. It has a different approach to state, one that involves immutability. Changes to state are modeled by carefully changing managed references so that they point to different immutable values over time. And because the Clojure runtime itself manages these references, it’s able to offer the programmer a great deal of automated support in their use.

First, data that needs to change must use one of the four options that Clojure offers. This makes it explicit to anyone reading the code in the future. Next, it offers a STM system that helps in making coordinated changes to more than one piece of data. This is a huge win, because it’s a lock-free solution to a hairy problem!

Clojure also offers agents and atoms, which allow independent changes to mutable data. These are different in that they’re asynchronous and synchronous, respectively, and each is useful in different situations. Finally, Clojure offers vars that can be used where changes to data need to be isolated within threads. The great thing is that despite offering options that are quite different from each other, they have a uniform way of creating and accessing the data inside them.

Clojure’s approach to state and mutation is an important step forward in terms of the current status quo of dealing with state and multithreaded programming. As we discussed in section 6.2, most popular OO languages confuse identities and state, whereas Clojure keeps them distinct. This allows Clojure to provide language-level semantics that make concurrent software easier to write (and read and maintain) and more resilient to bugs that afflict lock-based solutions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset