event handlers for spooling events to files or to a remote
process or host.
The behaviour libraries provide functionality for
dynamic debugging of a running program. They can be
requested to display the current behaviour state, produce
traces of messages received and sent, and provide statistics. Having this functionality automatically available
to all applications gives Erlang programmers a profound
advantage in delivering production-quality systems.
WORKER PROCESSES
Erlang applications can implement most of their functionality using long-lived processes that naturally fit a
standard behaviour. Many applications, however, also
need to create concurrent activities on the fly, often following a more ad-hoc protocol too unusual or trivial to
be captured in the standard libraries.
Suppose we have a client that wants to make multiple
server calls in parallel. One approach is to send the server
protocol messages directly, shown in figure 4A. The client
sends well-formed server call messages to all servers, then
collects their replies. The replies may arrive in the inbox
in any order, but collect_replies/1 will gather them in the
order of the original list. The client may block waiting for
the next reply even though other replies may be waiting.
This doesn’t slow things down, however, since the speed
of the overall operation is determined by the slowest call.
To reimplement the protocol, we had to break the
abstraction that the server behaviour offered. While this
was simple for our toy example, the production-quality
generic server in the Erlang standard library is far more
involved. The setup for monitoring the server processes
and the calculations for timeout management would
make this code run on for several pages, and it would
need to be rewritten if new features were added to the
standard library.
Instead, we can reuse the existing behaviour code
entirely by using worker processes—short-lived, special-pur-pose processes that don’t execute a standard behaviour.
Using worker processes, this code becomes that shown in
figure 4B.
We spawn a new worker process for each call. Each
makes the requested call and then replies to the parent,
using its own pid as a tag. The parent then receives each
reply in turn, gathering them in a list. The client-side
code for a server call is reused entirely as is.
By using worker processes, libraries are free to use
receive expressions as needed without worrying about
blocking their caller. If the caller does not wish to block,
it is always free to spawn a worker.
DANGERS OF CONCURRENC Y
Though it eliminates shared state, Erlang is not immune
to races. The server behaviour allows its application code
to execute as a critical section accessing protected data,
but it’s always possible to draw the lines of this protection
incorrectly.
For example, if we had implemented sequences with
raw primitives to read and write the counter, we would be
just as vulnerable to races as a shared-state implementation that forgot to take locks:
badsequence.erl
BAD - race-prone implementation - do not use - BAD
-module(badsequence).
-export([make_sequence/0, get_next/1, reset/1]).
-export([init/0, handle_call/2, handle_cast/2]).
API
make_sequence() -> server:start(badsequence).
get_next(Sequence) ->
N = read(Sequence),
write(Sequence, N + 1), BAD: race!
N.
reset(Sequence) -> write(Sequence, 0).
read(Sequence) -> server:call(Sequence, read).
write(Sequence, N) -> server:cast(Sequence, {write, N}).
Server callbacks
init() -> 0.
handle_call(read, N) -> {N, N}.
handle_cast({write, N}, _) -> N.
This code is insidious as it will pass simple unit tests
and can perform reliably in the field for a long time
before it silently encounters an error. Both the client-side
wrappers and server-side callbacks, however, look quite
different from those of the correct implementation. By
contrast, an incorrect shared-state program would look
nearly identical to a correct one. It takes a trained eye to
inspect a shared-state program and notice the missing
lock requests.
All standard errors in concurrent programming have
their equivalents in Erlang: races, deadlock, livelock,
starvation, and so on. Even with the help Erlang provides, concurrent programming is far from easy, and the
nondeterminism of concurrency means that it is always
difficult to know when the last bug has been removed.
Testing helps eliminate most gross errors—to the
extent that the test cases model the behaviour encountered in the field. Injecting timing jitter and allowing