Crossing borders: Concurrent programming with Erlang

Frontiers in concurrency

The Java™ programming language made starting a new thread easier than ever before. But freeing your concurrent programs of obscure bugs is a different matter, and Java's programming model might not be the best available. A language called Erlang is getting some good press now in the areas of concurrency, distributed systems, and soft real-time systems.

Share:

Bruce Tate (bruce.tate@j2life.com), President, RapidRed

Bruce TateBruce Tate is a father, mountain biker, and kayaker in Austin, Texas. He's the author of three best-selling Java books, including the Jolt winner Better, Faster, Lighter Java. He recently released Spring: A Developer's Notebook. He spent 13 years at IBM and is now the founder of the J2Life, LLC, consultancy, where he specializes in lightweight development strategies and architectures based on Java technology and Ruby.



18 April 2006

I love to kayak, and when I boat on technically demanding and dangerous rivers, I normally take an expert with me who can read rapids better than I can and point out dangers that I might otherwise risk. In this article, I'm going to be discussing concurrency, but because I'm not an expert on the topic I'm bringing one with me -- Brian Goetz, a well-respected concurrency guru in the Java community and a regular contributor here on developerWorks. Some of the examples in this article, and many of the concepts, are his. See Resources for links to several of his articles and his new book, which will be available this May.

About this series

In the Crossing borders series, author Bruce Tate advances the notion that today's Java programmers are well served by learning other approaches and languages. The programming landscape has changed since Java technology was the obvious best choice for all development projects. Other frameworks are shaping the way Java frameworks are built, and the concepts you learn from other languages can inform your Java programming. The Python (or Ruby, or Smalltalk, or ... fill in the blank) code you write can change the way that you approach Java coding.

This series introduces you to programming concepts and techniques that are radically different from, but also directly applicable to, Java development. In some cases, you'll need to integrate the technology to take advantage of it. In others, you'll be able to apply the concepts directly. The individual tool isn't as important as the idea that other languages and frameworks can influence developers, frameworks, and even fundamental approaches in the Java community.

The changing nature of concurrency

Concurrent programming is a demanding exercise, and distributed, concurrent programming demands the most out of even the best developers. Java documentation often includes alarming warnings about using multithreading, frequently asking users to avoid concurrency at all costs for certain libraries. The Java community does not always do a good job of dealing with concurrency, but so far we've gotten off easy, for several reasons:

  • Most of the applications most developers write run on systems with few processors.
  • The programming paradigms you may have learned in college and beyond match the way the Java language works.
  • You can often rely on faster hardware, rather than improved concurrency, for performance.

Our free pass will expire soon, though. Brian Goetz, one of the industry's top concurrency experts, has written about high-performance Java, including concurrency, for more than a half a decade now. Brian believes that you're likely to see a significant increase in concurrency problems, thanks to the laws of physics, subsequent demands on programmers, and shortcomings in the current model.

Moore's Law predicts that the number of transistors on a chip doubles every two years. Semiconductor manufacturers are about to hit some physical limits that will impact their ability to double the transistors on a single wafer, so you'll see matrices of processors. That change will force more concurrent programming, bringing more developers kicking and screaming into distributed programming.

Concurrency is tough; Brian has an effective analogy: If you've got a white carpet in the living room, you can tell your 3-year-old child not to spill grape juice in the living room. If you are lucky enough to have a child who can follow that rule, you might survive the experience with a white carpet with purple dots instead of a purple carpet with white dots. But most 3-year-olds can't follow that rule, so you need a better one: Don't bring the grape juice into the living room. Extending this analogy to the issue at hand, by encapsulating concurrency, you can't get it wrong; by forcing clients to deal with concurrency, you risk spills when they can't follow the instructions.


Concurrency in Java technology

Java support for concurrency is like the first rule: Don't spill grape juice on the carpet. Consider a simple counter program from Java Concurrency in Practice (see Resources), shown in Listing 1:

Listing 1. A simple counter program
public class UnsafeSequence {
  private int value;
  /** Returns a unique value. */
  public int getNext() {
    return value++;
  }
}

You might use this program to create unique identifiers. It is correct if you're using it from within a single thread. If you're not, two applications could access the counter at precisely the same time and get the same value.

You can make mistakes with concurrency in at least two areas: atomicity and visibility. Atomicity means that certain pieces of an application must all be executed as one unit. For example, the counter in Listing 1 reads a value, adds 1 to it, and writes the result. Because it doesn't limit access to one thread at a time, it enables a concurrency bug. If you somehow made that piece of code atomic, the bug would disappear.

Visibility means that you want changes that you make to a value to be visible precisely when you intend them to be. Microprocessors have caches, and with some designs, it's easy to read old values unless you explicitly protect against that problem.

The Java solution is the synchronized keyword. Think of synchronized as a lock on a certain resource. Unfortunately, Java's concurrency controls are easy to introduce but difficult to use well. You can protect a resource quite easily with synchronized, but the keyword is also easy to misuse in at least three ways:

  • You can forget to synchronize a resource that should be synchronized. This problem is like forgetting to lock your treasure chest.
  • You can synchronize the wrong resource. This problem is like locking a treasure chest, but not the one with your treasure in it.
  • You can use synchronized too often. This problem is like locking your treasure chest and forgetting the combination. You won't have enough access to your treasure.

To further illustrate the problem, consider a common simple problem: You want to move an item from one collection to another. It's not enough to synchronize each collection independently; you need to synchronize the transaction that moves data from one to another. Most Java developers do not go to this trouble, either because they are ignorant of the danger or they trust that synchronizing one collection will provide enough protection. This reasoning will lead to significant problems as the number of processors increases with new parallel architectures.


Theory and practice

If you've taken any advanced courses on concurrency or distributed systems, you probably noticed that the mathematical languages used to teach concurrency theory usually do not match the programming languages developers use. Teachers generally use some form of a functional language to teach concurrency concepts.

In other fields, programming techniques and architectures whose programming models match theory do very well. Take databases, for example. Relational database theory is based on a mathematical language called relational algebra. SQL provides the primitives needed by relational algebra. On the strength of relational database optimization, relational databases have surpassed other database models, such as network, hierarchical, and object-oriented databases.

Or consider IDEs. Until recently, most IDEs worked by letting you edit text and then compiling that text into programs. IntelliJ's IDEA changed all of that by editing the abstract syntax tree (AST) directly. This tree is the intermediate form that compilers use to generate code. By letting IDEA work directly with the AST, IDEA could provide much better refactoring support than other IDEs. When you renamed a piece of text, you were actually updating the symbol in the AST, and IDEA could change every block of code that used that symbol. Now, the best IDEs all use this approach.

Erlang: From theory to practice

Some computer scientists wondered what might happen if they programmed concurrent, distributed systems in a language that closely matches the programming theory. In 1982, Ericsson, a leading telecommunications company, began an extensive study on the impact of programming languages on the development of applications exploiting concurrency. Over the years of study that followed, it found that symbolic languages such as Lisp and Prolog tend to lead to much more productive programming, but languages that have built-in notions of concurrency (such as ultra-lightweight processes, active error recovery, and distributed message passing) tend to work much better than those that don't. Unfortunately, no language could satisfy both requirements.

In 1987, Ericsson experimented with the first early prototypes of Erlang. It presented its findings three years later at the ISS 90 (Information Sciences and Systems) conference. In 1993, Erlang was extended to support distributed systems, and it has grown steadily ever since.

Today, Erlang is a robust language that's not dominant by any stretch, but dozens of large manufacturers and telecoms use it. The language's focus is on very large, soft real-time distributed applications. It has a reputation for being robust, stable, and highly productive. Productivity and stability are not traits most developers would ascribe to distributed systems of any kind.


Functional programming in Erlang

Because advanced concurrency theory usually uses mathematical functions to describe program execution, it stands to reason that functional languages might be used effectively. Functional programming is a programming style that emphasizes evaluating expressions rather than executing commands. If you haven't programmed in a functional language before, the code examples might seem alien or even shocking, but functional languages like Lisp have proven to be very productive in a variety of settings.

Erlang is a functional language. An Erlang program describes a series of functions. Each function uses pattern matching to determine which function to execute. For example, suppose you want to compute a factorial, which is an integer multiplied by the previous integer, and so on all of the way down to 0, with one exception -- the factorial of 0 is 1. (If you want to code along with me, you can download the language and environment for free; see Resources.) You'd define a factorial this way in Erlang:

fact(0) -> 1;
fact(N) -> N * fact(N-1).

You see two statements, separated by a semicolon. When you invoke fact, Erlang matches the argument with the correct function. In this case, fact(0) invokes the first function, and everything else matches the second. A period terminates the definition. So Erlang's factorial closely resembles the mathematical definition.

Here's another example. In a Fibonacci sequence, each number is the sum of the preceding two numbers. The first two numbers are 1. In Erlang, to compute the nth term of a Fibonacci sequence, you'd do this:

fib(1) -> 1;
fib(2) -> 1;
fib(X) -> fib(X-1) + fib(X-2).

Packaging and running your code

Now that you've seen some examples, you can package them up and run some tests. You can combine these programs into a single module called math.erl and export both functions for use:

Listing 2. Exporting functions
-module(math).
-export([fact/1]).
-export([fib/1]).

fact(0) -> 1;
fact(N) -> N * fact(N-1).

fib(1) -> 1;
fib(2) -> 1;
fib(X) -> fib(X-1) + fib(X-2).

Each module must declare the functions that can be accessed by code outside of the module. In this case, Erlang applications can call fact and fib, which both take one argument, from outside of the math module. You can then start the environment and execute both functions:

c(math).
math:fib(10).
math:fact(10).

The first line compiles the program into math.beam. The name of the module is math. The names of the functions are fact and fib. Now, you're ready to dig a little deeper.


Tuples and lists

Object-oriented programmers structure their data and code into objects. You've seen how Erlang programmers use modules to organize code. An Erlang developer uses tuples and lists to organize even very complex data structures.

Atoms are single values that can be symbols, constants, numbers, or strings. (Think of a symbol as a name for something real or abstract.) A tuple has a fixed number of elements, and a list has an indefinite length. You wrap tuples in braces and lists in brackets:

atom
16
4.6
[this, is, a, list, that, can, go, on, and, on]
{a, tuple}
[
{{word, tuple}, 
  {definition, "a fixed length ordered collection of atoms"}},
{{word, erlang}, 
  {definition, "a functional language optimized for concurrency"}}
]

Strings are shorthand for a list of integers representing the characters in the string. In the Java language, you'd use an iterator or an index to traverse a data structure. In Erlang, you traverse data structures with functions, usually using recursion. For example, to sum up the contents of a list, you'd do this:

sum([H|T]) -> sum(T) + H;
sum([]) -> 0.

You can see typical Erlang recursive strategies in action here. H stands for head, and T stands for tail. The first function takes the first list element and adds it to the sum of all of the elements in the tail. The sum of an empty list is zero. Using this strategy, you can do just about anything you want with the elements of a data structure.


Erlang and concurrency

As in the Java language, Erlang can easily start concurrent tasks but uses lightweight processes instead of threads. For example, if you want to spawn a process to compute a very large factorial using your new math module, you use the spawn keyword:

Pid = spawn(math, fact, [1234]).

Pid is the process ID, math is the module, fact is the function, and 1023456 is the argument to the function. The process ID is useful because you can use it to send messages. To send a message, you need a sender and a receiver. The sender uses the ! operator on a process ID, followed by a message:

Pid ! {hi, mom}.

The receiver can then receive a message. Suppose you want to print a message to the screen until you get a stop message. You'd call a function called loop and then receive a message with a keyword. You'd then print the message or stop based on the contents of the message and then call loop again:

loop() ->
  receive
      {From, Message} -> 
         io:format("Received: ~w~n",[Message]),
         loop();
      stop ->
         true
  end.

This is another typical Erlang strategy. The receive keyword tells Erlang to listen for a message. Then, the incoming message is matched against a series of patterns. The stop symbol goes to the second function, and everything else goes to the compound statement that prints the contents of the message (~w is a word, and ~n is a newline) and then calls loop again for the next statement.

Defensive programming

The Erlang philosophy of error handling is dramatically different from the Java philosophy. Usually, you'll see this philosophy described as avoiding defensive programming. I prefer a different explanation. In Java programming, defensive programming is passive. You try to keep things from going wrong. With Erlang, defensive programming is much more active. You let the system detect what goes wrong and deal with the problem.

Because processes are so lightweight, you'd typically use many of them. Rather than trying to keep each process alive, such as getting a conversion error as you're reading user input, you let a process fail and let an Erlang supervisor detect the problem and restart that part of the system. The result of this strategy is a simplified programming model that keeps error detection and error handling separate, ultimately leading to a more productive programming experience and programs that are more fault tolerant.


Mapping Erlang to concurrency theory

Being a functional language, Erlang maps onto modern concurrency theory extremely well. Erlang also has some other principles that play well with building large, correct concurrent applications:

  • Single assignment. Erlang lets you assign a value to a variable exactly once. All Erlang variables are immutable after their initial assignment.
  • Lightweight processes. Because Erlang uses lightweight processes instead of threads, programmers can fork processes easily and efficiently. Unlike threads, processes share no common resources, so Erlang programmers don't have to worry as much about contention for the same resources.
  • Message passing. In Erlang, message passing is easy, and it's also the only allowable communication between processes.

So Erlang simplifies the math behind building correct programs because the interactions between modules are all explicit and well defined. Also, the property of single assignment eliminates several kinds of common concurrency bugs.

I've written only a few programs in Erlang that were only marginally more than trivial applications, but it struck me for the first time I could directly employ many of the techniques that I learned in my distributed-systems coursework at the University of Texas. I'll never be a concurrency expert, but in this case, a programming language brought some theory to life for me.


Bringing it home

Of course, Erlang is a niche language. We're not all going to be programming in it in the next year or two. But Erlang does illustrate the need for providing a simpler model for concurrency in Java programming. Brian Goetz has long advocated giving up some concurrency to build a simplified, unified concurrency model that's easier to understand and use. Check out his FindBugs approach, which defines a set of annotations, rules, and analysis tools that makes it easier to know if an application will work under concurrent conditions (see Resources). In Java Concurrency in Practice, Brian and some of the other top concurrency experts in the industry further this work and strive to bring us to a better understanding of how to write concurrent applications with better performance and stability.

But most concurrency experts say even that technique doesn't go far enough. Much of the research deals with new models that are easier to understand and can be proven correct, with less responsibility on the programmer. One such technique, called transactional memory, treats memory like database records (see Resources). By using techniques similar to a database's optimistic concurrency control, an application could time stamp operations in memory and run unimpeded, rolling back operations should a conflict occur.

Regardless of the technique that we use, we'll need to get much better, and quickly. As multiprocessor technologies become more prominent, we'll see a greater demand on techniques, architectures, and possibly even languages that make concurrency easier to manage.

In the next article, I'll look at the secret sauce behind Ruby on Rails -- the characteristics that make the framework so productive. I'll also talk about some projects closer to Java technology that attempt to provide the same experience.

Resources

Learn

Get products and technologies

  • Erlang: Download the Erlang language and environment.

Discuss

  • Multithreaded Java programming: If you have questions about concurrency in the Java language, stop by this discussion forum, moderated by Brian Goetz.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=110153
ArticleTitle=Crossing borders: Concurrent programming with Erlang
publish-date=04182006