Concurrency
Concurrent computers work on more than one thing at once.
The Wikipedia
article on parallel computing provides an overview.
Concurrent execution can be loosely grouped into three implementation
categories, in order of loosest to tightest coupling.
- Distributed concurrent computation (cluster computing or MPP
(massive parallel processing) or grid computing) - on multiple computers which
share no memory and are sending messages between each other
- Distributed shared memory (aka SMP, symmetric multiprocessor) -
different processes running on different processors but sharing
some special memory via a memory bus.
- Multithreaded computation - multiple threads of execution
sharing a single memory which is local.
The Wikipedia
article on threads clarifies how threads and processes differ.
Forms of concurrency we are not covering
- Vector processing - computers that can do an operation on a
whole array in one step. Used to be called SIMD (single
instruction multiple data).
- Stream processing - the modern version of vector processing
which can work on more than just arrays in parallel (e.g. sparse
arrays). These guys are called GPGPUs (general purpose
graphics processing units) since they arose as generalizations
of specialized graphics processors.
- FPGA's - field programmable circuits: create your own
(parallel) logic circuitry on the fly.
PLs and concurrency
- Historically, concurrent programming arose as various library
extensions to existing languages.
- Early models included UNIX sockets for IPC, and process forking
in C.
- While some concurrency is indeed best as libraries, some should
belong in the PL.
Threads in general
- Multithreaded programming is coming on in a big way - newer CPUs
have multiple cores and can run multiple threads simultaneously.
- Mulithreaded programming is also a disaster waiting to happen -
the programs are just too hard to debug.
- Main problem is the bizarre cases of overlap that can occur
when accessing the shared memory.
The main Bad Things:
- Race condition: two operations are interleaved and leave the
data in an inconsistent state. Example: double variable update
where one thread set the hi word and another thread set the low word
due to near- simultaneous access - the double's value is not a
sensible value from either thread.
- Deadlock: threads may need to wait for resources to free up (they
were locked to begin with to prevent race conditions), and can get in a
cycle of waiting: A waits for B waits for C waits for A
Locking
Avoiding races with locks of various kinds
- Monitors - regions of mutual exclusion in the source program,
only one thread can execute a monitor block at any one point.
Similar to synchronized in Java.
- Semaphore - a low-level locking mechanism: grab a lock before a
critical operation; block if someone else has lock; once you
have the lock you know you are the only one accessing; free it
when you are done. General semaphores allow n threads to
simultaneously access, not just 1.
Atomicity
Atomicity is a key design concept
- An atomic region is a region of code that may have been interleaved with
other thread executions, but you can't tell -- it always
appears to have run atomically (in one step)
- The more atomicity you can get in your language design the fewer
interleavings need to be considered in debugging and the fewer bugs.
Java Threads
The Java concurrency tutorial.
- An implementation of the standard notion of thread: shared
memory but concurrent threads of control (i.e., multiple runtime
stacks).
synchronized declarations declare zones of mutual
exclusion; they are a form of monitor
Synchronized in detail
- They can be declared as
synchronized methods, or as
blocks of code.
- When a synchronized method/block is running, no other synchronized
method/block for that object can start from another thread -
any such threads have to
wait until the currently running method/block finishes.
- Plus: object-based which is a good level of abstraction for locking
- Minus: only gives mutual exclusion, not atomicity
Other Java concurrency control features. Much is in the new
java.util.concurrent package.
- Atomic integers, floats, etc - there will never be any race
conditions on setting or getting the value from
AtomicInteger etc.
- Locks -
java.util.concurrent.locks.Lock
- Concurrent collections - e.g.
ConcurrentHashMap
which supports concurrent add/lookup atomically.
Problems with Java threads
- lack of atomicity mentioned above
- complexity of Java memory model: need to know how operations
that are not atomic may mess things up, e.g. if "half an
integer" is written.
The Actor Model
History: Hewitt's idea; elaborated by many others including yours truly.
- Actors are autonomous distributed agents
- Actors have names which are not forgeable
- asynchronous messages; arrival order nondeterminism
- local state only
- actors can create other actors
- If an actor is busy when a message arrives is it put in a
message queue.
- No faults: all messages eventually arrive (but they may take
arbitrarily long)
- Finite local processing: each actor processes each message in a
finite amount of time.
- Atomicity: multiple actors can be running in
parallel, but any interleaved run is equivalent to a run where
each actor runs all of its steps in one big step.
AFbV: Actors on FbV
We will add an actor layer on top of the FbV language:
AFbV. We need the "V" to have variants to define
messages. Recall FbV variants are like OCaml's
inferred variants - `foo(4) is the variant
foo with argument 4. Notice how we can also
view this as the message foo with argument
4. FbV variants always have exactly one
argument only, for simplicity.
Syntax of AFbV
AFbV expressions are the following.
e ::= ( ... all the FbV stuff ) | e <- e | Create(e,e') | a
where a are the atomic actor names. They are like the cells
c of DS, they cannnot appear in source
programs but can show up at runtime, and there are infinitely many
unique ones; they are just names (nonces).
e <- e' is a message send and expects e to be an actor name,
and sends it the message which is the value of e'.
Create(e,e') creates an actor with behavior
e, and with initial data e'. e should evaluate to a function
and that function is the (whole) code for the actor. The
create returns the (new) name of this new actor as
its result.
Some unusual aspects of AFbV
- Each message response is functional -- there is no state within
an actor in the form of fields.
- The code of an actor is nothing but one function. You write
your own dispatch code using the
Match of
FbV.
This is the dual encoding to objects as
records -- its objects as functions and messages as variants.
- In order to allow actors to know themselves, at creation time
they get passed their own name.
- At the end of processing each message, the actor goes idle; the
behavior it is going to have upon receiving the next message
is the value at the end of the previous message send.
An Example
Before getting into the operational semantics lets do an example.
Here is an actor that gets a start message and then counts down from
its initial value to 0:
Function myaddr ->
Y (Function this -> Function localdata -> Function msg ->
Match msg With
`main(n) -> myaddr <- `count(n); this(_)
| `count(n) -> If n = 0 Then this(_) Else
myaddr <- `count(n-1);
this(_) /* set the function to respond to next message */
Here is a code fragment that another actor could use to fire up a new
actor with the above behavior and get it started. Suppose the above
code we abbreviated CountTenBeh.
Let x = create(CountTenBeh,_) /* _ is the localdata - its unused in this example */
In x <- `main(10)
Here is an alternative way to count down, where the
localdata field holds the value, and its not in the message.
Function myaddr ->
Y (Function this -> Function localdata -> Function msg ->
Match msg With
`count(_) -> If localdata = 0 Then _ Else
myaddr <- `count(_);
this(localdata - 1) /* set the function to respond to next message/
Suppose the above
code was abbreviated CountTenBeh2; using it is then
Let x = create(CountTenBeh2,10) /* 10 is the localdata */
In x <- `count(_)
The latter example is the correct way to give actors local data -- in
the former example the counter value had to be forwarded along every message.
Here is another usage fragment for the first example:
Let x = create(CountTenBeh,_) /* _ is the localdata - its unused in this example */
In x <- `main(10); x <- `main(5)
In this case the actor x will in parallel and
independently counting down from 10 .. 0 and 5 .. 0 - these counts may
also interleave in random ways. For the second example an analogue
might be:
Let x = create(CountTenBeh2,10) /* 10 is the localdata */
In x <- `count(_); x <- `count(_)
This does nothing but get one more count message queued up; since the
actor sends a new one out each time it gets one until 0, the effect
will be to have a leftover message at the end.
Operational Semantics of Actors
The operational semantics for actors has two layers: the local
computation, which is not to different than FbV, and
the concurrent global stepping of all the actors. Lets do the
latter first.
- A global state G is a soup of the active actors and
sent messages (note we are using "U" to mean set union here):
{ <a,v> | a is an actor name,
v is its behavior } U
{ [a <- v] | a is an actor name,
v is the message sent to a }
- Actor systems can run forever, there is no notion of a final
value. So, the system does small steps of computation:
G1 --> G2 --> G3 --> ...
-- this indicates one step of computation; in each single step one
actor competely processed one message.
- -->* is then the reflexive, transitive closure of --> --
many steps of actor computation.
- In general this continues
infinitely since actor systems may not terminate. The final
meaning of a run of an actor system is this infinite stream of states.
The Local Rules
Lets start with the local rules. They are defined with a similar
relation ==> as in FbV operational semantics, but
the local executions additionally have side effects of the
actors they create and messages they send. We will make any such side
effects be labels on this arrow relation. So we have
- Local compuration relation
==>^S (S
should be written on the top;
html won't allow that easily)
- S here is the soup of effects; in fact each S is a subset of a
G. It contains two kinds of elements,
[a <-v] indicating a send to a
of message vthat this local
actor performed over its execution, and
<a,v> indicating a new actor it
created, named a, with behavior (body)
v.
Here then are the rules for ==>^S. Most of the rules
are nearly identical to FbV, we just give the
+ rule to show the change:
e ==>^S n e' ==>^S' n'
--------------------------
e + e' ==>^(S U S') n'' where n'' is the sum of integers n and n'
- since e/e' could in theory have each created actors or sent
messages, we need to append their effects to the final result. These
effects are like state, they are on the side. A major difference with
DS is the effects here are "write only" -- they
don't change the local computation in any way, they are only spit
out. In that sense local actor computation stays functional.
Here is the send rule:
e ==>^S a e' ==>^S' v
---------------------------------
e <- e' ==>^(S U S' U {[a <-v]}) v
The main consequence is the message [a <-v] is added
to the soup. (The return result v here is largely irrelevant, the goal
of a message send is the side effect added to the list.)
Here is the create rule:
e ==>^S v e' ==>^S' v' v a v' ==>^S'' v''
-----------------------------------------------
Create(e, e') ==>^(S U S' U S'' U {<a,v''>}) a for a a fresh actor name
This time the return result matters - it is the name of the new
actor. The running of v a v' passes the actor its own name and its initial values to
initialize it.
The Global Rule
Here is the global single-step rule for one actor in the soup
processing in its entirety one message:
(G U {[a <- v']} U {<a,v>}) -->
(G U {<a,v''>} U S )
if (v v' ==>^S v'')
This is the only global rule. It matches an actor with a message in
the global soup that is destined for it, uses the local semantics to
run that actor (in isolation), and throws back into the soup all of
the S, which contains all the messages sent by this one actor run as
well as any new actors created by this one actor run. A global actor
run is just the repeated application of this rule. Notice how the
actor behavior which was v is changed to
v'', the result of this run.
To test these rules you can run the example programs above.
The Atomicity of Actors
- The above semantics has actors executing atomically: each actor runs
independently to completion.
- However, it would be possible to make an
alternative semantics in which multiple actors run in parallel
- Since the actors are completely local in their executions, the
two semantics should be provably equivalent.
Last modified: Fri May 1 13:32:17 EDT 2009