A repeated theme in this book is the need for better language constructs and programming models to handle concurrency. Across the languages, the approaches were often strikingly different but extremely effective. Let’s walk through some of the approaches we saw.
By far, the most common theme in the concurrency discussion was the programming model. Object-oriented programming allows side effects and mutable state. Taken together, programs got much more complicated. When you mix in multiple threads and processes, the complexity got too great to manage.
The functional programming language adds structure through an important rule. Multiple invocations of the same function lead to the same result. Variables have a single assignment. When side effects go away, race conditions and all related complexities also go away. Still, we saw tangible techniques that went beyond the basic programming model. Let’s take a closer look.
Whether using an object or a process, the actor approach is the same. It takes unstructured interprocess communication across any object boundary and transforms it onto structured message passing between first-class constructs, each one supporting a message queue. The Erlang and Scala languages use pattern matching to match inbound messages and conditionally execute them. In Chapter 6, Erlang, we built an example around Russian roulette to demonstrate a dying process. Recall that we put the bullet in chamber 3:
| erlang/roulette.erl | |
-module(roulette).
|
|
-export([loop/0]).
|
|
|
|
% send a number, 1-6
|
|
loop() ->
|
|
receive
|
|
3 -> io:format("bang.~n"), exit({roulette,die,at,erlang:time()});
|
|
_ -> io:format("click~n"), loop()
|
|
end.
|
|
We then started a process, assigning the ID to Gun. We could kill the process with Gun ! 3. The Erlang virtual machine and language supported robust monitoring, allowing notification and even restarting processes at the first sign of trouble.
To the actor model, Io added two additional concurrency constructs: coroutines and futures. Coroutines allowed two objects to multitask cooperatively, with each relinquishing control at the appropriate time. Recall that futures were placeholders for long-running concurrent computations.
We executed the statement futureResult := URL with("http://google.com/") @fetch. Though the result was not immediately available, program control returned immediately, blocking only when we attempted to access the future. An Io future actually morphs into a result when the result becomes available.
In Clojure, we saw a number of interesting approaches to concurrency. Software transactional memory (STM) wrapped each distributed access of a shared resource in a transaction. This same approach, but for database objects, maintains database integrity across concurrent invocations. We wrapped each access in a dosync function. With this approach, Clojure developers can break away from strict functional designs where it makes sense and still have integrity across multiple threads and processes.
STM is a relatively new idea that is just creeping into more popular languages. As a Lisp derivative, Clojure is an ideal language for such an approach because Lisp is a multiparadigm language. Users can use different programming paradigms when they make sense with the confidence that the application will maintain integrity and performance, even through highly concurrent accesses.
The next generation of programmers will demand more out of his language. The simple wheel and stick that let you start a thread and wait on a semaphore are no longer good enough. A newer language must have a coherent philosophy supporting concurrency and the tools to match. It may be that the need for concurrency renders whole programming paradigms obsolete, or it may be that older languages will adapt by using stricter controls for mutable variables and smarter concurrency constructs such as actors and futures.