Zlib - Pub Concurrent-Programming
Zlib - Pub Concurrent-Programming
Concurrent Programming
C. R. Snow
University of Newcastle upon Tyne
Preface
1 Introduction to Concurrency 1
1.1 Reasons for Concurrency 2
1.2 Examples of Concurrency 4
1.3 Concurrency in Programs 6
1.4 An Informal Definition of a Process 7
1.5 Real Concurrency and Pseudo-Concurrency 9
1.6 A Short History of Concurrent Programming 10
1.7 A Map of the Book 11
1.8 Exercises 13
2 Processes and the Specification of Concurrency 15
2.1 Specification of Concurrent Activity 15
2.2 Threads of Control 16
2.3 Statement-Level Concurrency 19
2.3.1 Concurrent Statements 19
2.3.2 Guarded Commands 21
2.3.3 CSP and OCCAM 25
2.4 Procedure-Level Concurrency 25
2.5 Program-Level Concurrency 30
2.6 The Formal Model of a Process 34
2.7 Examples of Process States 37
2.7.1 Motorola M68000 Processor 37
2.7.2 A Hypothetical Pascal Processor 38
2.8 Exercises 40
3 Communication between Processes 42
3.1 Interference,Co-operation and Arbitrary Interleaving 42
3.2 The Critical Section Problem 46
3.3 Solutions to the Critical Section Problem 49
3.4 Dekker's/Peterson's Solution 53
3.5 A Hardware-Assisted Solution 55
vi Contents
C.R.Snow
Newcastle upon Tyne, June 1991.
1
Introduction to Concurrency
Concurrency has been with us for a long time. The idea of different
tasks being carried out at the same time, in order to achieve a
particular end result more quickly, has been with us from time
immemorial. Sometimes the tasks may be regarded as independent of
one another. Two gardeners, one planting potatoes and the other
cutting the lawn (provided the potatoes are not to be planted on the
lawn!) will complete the two tasks in the time it takes to do just one of
them. Sometimes the tasks are dependent upon each other, as in a
team activity such as is found in a well-run hospital operating theatre.
Here, each member of the team has to co-operate fully with the other
members, but each member has his/her own well-defined task to carry
out.
Concurrency has also been present in computers for almost as
long as computers themselves have existed. Early on in the
development of the electronic digital computer it was realised that
there was an enormous discrepancy in the speeds of operation of
electro-mechanical peripheral devices and the purely electronic
central processing unit. The logical resolution of this discrepancy was
to allow the peripheral device to operate independently of the central
processor, making it feasible for the processor to make productive use
of the time that the peripheral device is operating, rather than have to
wait until a slow operation has been completed. Over the years, of
course, this separation of tasks between different pieces of hardware
has been refined to the point where peripherals are sometimes
controlled by a dedicated processor which can have the same degree of
"intelligence" as the central processor itself.
Even in the case of the two gardeners, where the task that each
gardener was given could be considered to be independent of the other,
there must be some way in which the two tasks may be initiated. We
2 Introduction to Concurrency
may imagine that both of the gardeners were originally given their
respective tasks by the head gardener who, in consultation with the
owner of the garden, determines which tasks need to be done, and who
allocates tasks to his under-gardeners. Presumably also, each
gardener will report back to the head gardener when he has finished
his task, or maybe the head gardener has to enquire continually of his
underlings whether they have finished their assigned tasks.
Suppose, however, that our two gardeners were asked to carry
out tasks, both of which required the use of the same implement. We
could imagine that the lawn-mowing gardener requires a rake to clear
some debris from the lawn prior to mowing it, while the potato planter
also requires a rake to prepare the potato bed before planting. If the
household possessed only a single rake, then one or other gardener
might have to wait until the other had finished using it before being
able to complete his own task.
This analogy serves to illustrate the ways in which peripheral
devices may interact with central processors in computers. Clearly if
we are asking for a simple operation to take place, such as a line
printer skipping to the top of the next page, or a magnetic tape
rewinding, it suffices for the Central Processing Unit (c.p.u.) to initiate
the operation and then get on with its own work until the time when
either the peripheral device informs the c.p.u. that it has finished (i.e.
by an interrupt), or the c.p.u. discovers by (possibly repeated)
inspection that the operation is complete (i.e. by polling).
Alternatively, the peripheral device may have been asked to read a
value from an external medium and place it in a particular memory
location. At the same time the processor, which is proceeding in its
own time with its own task, may also wish to access the same memory
location. Under these circumstances, we would hope that one of the
operations would be delayed until the memory location was no longer
being used by the other.
has been evaluated that the sub-expression a*6 can be added, and then
finally the evaluation of the whole expression can be completed.
It is in the field of operating systems where concurrent
programming has been most fruitfully employed. A time-sharing
operating system, by its very nature, is required to manage several
different tasks in parallel, but even if the system is only providing
services to a single user it will be responsible for managing all the
peripheral devices as well as servicing the user(s). If the concurrent
programming facilities can also be offered to the user, then the
flexibility of concurrency as a program structuring technique is also
available to application programs. It is not our intention in this book to
deal with operating systems as a subject, but it will inevitably be the
case that operating systems will provide a fertile source of examples of
concurrent programs.
x:= x + 1;
This statement should really only be used if there has been a previous
statement initialising the variable JC, e.g.
x: = 0;
Figure 1.2
and to be running the same code, i.e. one program can be associated
with two distinct processes simultaneously. Conversely, if two
programs were to be run strictly sequentially but making use of the
same storage area, we might regard them as belonging to the same
sequential process. Thus processes and programs are not the same,
although clearly "process" would be a somewhat empty concept
without an associated program. In the case where two processes are
executing identical code, and that code is pure, i.e. is never modified as
a result of its own execution, then some machines or systems may
permit two (or more) processes to access and execute the code from the
same physical storage. This has the virtue of saving on actual storage
used, but conceptually one should think of separate processes as being
totally disjoint and occupying distinct areas of storage.
The question then arises as to how processes are created and
how, if at all, they cease to exist. The simplest approach to this
problem is to imagine that processes are brought into existence as the
system begins to operate, and to continue to exist until the whole
system is halted. Such a model has great virtue in the consideration of
an operating system, which typically is required to provide services
continuously from the starting up of the system to the halting of the
machine. Similarly, a general concurrent program may have a fixed
number of processes which are initiated as soon as the program starts
to run, and remain in existence until the whole program terminates.
Some programming systems, usually in collaboration with the
underlying operating system, are capable of creating and destroying
processes dynamically. In such cases, the concurrent program clearly
has much more flexibility with regard to the way in which processes
may be created and destroyed according to the needs of the whole
program, and may affect the overall structure of the program.
1.8 Exercises
(a), sequentially,
(b). concurrently, as defined by the tree structure (assuming
that sufficient processors are available to allow maximal
concurrency).
1.3 Repeat exercise 1.2, given that the time taken by each
operation is:
+ - 1 unit
* - 2 units
** - 4 units.
1.5 For the program given in exercise 1.4, and using the partial
ordering found, draw the corresponding acyclic graph.
decrements this counter. Only the join which causes the counter to be
decremented to zero actually causes control to pass to x.
The fork sets up a separate thread of execution, the code for
which is stored at the address designated in the fork instruction, while
at the same time, the original thread of execution continues
uninterrupted. In this sense, and only in this sense, the new thread of
control is inferior to the original thread of control, but for reasons
related to subsequent refinements of the fork/join mechanism, we shall
refer to the new thread of control as being the child, and the original as
the parent. Figures 2.1(a) and (b) are intended to illustrate
Figure 2.1(a)
fork
parent .
child t_
diagrammatically the action of fork and join. In figure 2.1(a), the fork
creates the child, and the parent continues along its normal execution
path until it reaches the join. Because the child has not yet completed
its execution, i.e. in terms of the original proposal it has not yet
reached its join instruction, the parent has to wait (or merely
terminate), and the child is responsible for causing execution to
resume correctly at the end of its code. Figure 2.1(b) illustrates the
alternative situation where the child reaches its join first and simply
Figure 2.1(b)
fork join
parent . A—
child
child
terminating
Processes and the Specification of Concurrency 19
with < statement list> having the usual obvious meaning. Even in
sequential programming, a statement list may have different
interpretations depending upon the construct in which the list
appears. In an ordinary block, however, we would expect each of the
statements in the list to be executed in sequence, each one beginning
to execute when its predecessor is complete, the whole statement being
complete (i.e. the end being reached) when the last statement in the
list has finished executing.
The action of the concurrent statement implies a certain
synchronisation of concurrent activity, both when the concurrent
statement begins, and when it is complete. All of the statements in the
list associated with the concurrent statement must be thought of as
beginning their execution at exactly the same instant of time. (In
practice, this will only rarely be the case, but any delays in starting to
execute any of the statements will be random and unpredictable, so
that not only the precise moment at which each begins to execute will
be impossible to determine, but so will the order in which the
statements begin.) This operation is analogous with a fork being
executed to initiate the execution of each statement in the list, except
that a fork can only initiate one additional thread of control, and a
sequence of forks would determine the order in which the statements
began their execution. The timing, and therefore the order, of the
starting of each statement within the cobegin/coend block is
arbitrary.
The coend has a role in the synchronisation mechanism too. If
we assume that a concurrent statement might be included in a larger
program, which would be the case if the concurrent statement should
happen to be a part of a larger sequential activity, it will be necessary
for the termination of the concurrent statement to be well defined. The
semantics of the concurrent statement must include the requirement
that the end of the statement occurs only when each of the members of
the statement list has terminated. It is therefore necessary for the
Processes and the Specification of Concurrency 21
begin
A;
cobegin
B;
begin
cobegin
C;D
coend;
E
end
coend;
cobegin
begin
G;I
end;
begin
#;J
end
coend
end
Program 2.1
framework for reasoning about non-determinism within programs.
The specific example which Dijkstra was considering at the time he
invented guarded commands was the problem of finding the greatest
common divisor of two positive integers using Euclid's algorithm.
Euclid's algorithm is based on the observation that the
greatest common divisor, gcd (JC, y), of two positive integers x and y is
given by:
gcd: = x:
whilex <> ydo
begin
ifx > y thenjc:= x - y
else{y > x}y:= y-x;
gcd: = x
end;
Program 2.2
Guarded commands become a powerful tool only when they are
combined together using the alternative construct and the repetitive
construct.
The alternative construct introduces the non-determinacy by
allowing a number of guarded commands to be grouped together, and
allowing any one of the statements to be executed, provided its guard
evaluates to true. Only one of the statements is executed, and in this
sense the construct appears to be similar to a case statement in many
conventional programming languages. The non-determinacy comes
about because it is not essential that only one of the guards is true at a
time, such as is the case in ADA for example, and there is no
algorithmic rule which determines which statement (or command)
should be executed (such as in the Mesa SELECT statement or the
switch statement of the C programming language, in which the
"guards" or selectors are scanned in lexical order within the program
text until a true value is found).
To exemplify the alternative construct in a very simple way,
consider the problem of assigning to a variable m the value of the
larger of two variables x and y. In a conventional programming
language notation, one might write:
This will obviously give the correct answer, but there is a slight
asymmetry in this solution due to the fact that we actually have three
separate relations between x and y to consider, namely x>y, x<y and
x=y. In this instance, we would of course argue that it does not matter
whether we test for x>y or x>y, since the result will be the same.
However, the guarded command notation allows us to write down a
symmetric solution, and allow a non-deterministic choice during the
24 Processes and the Specification of Concurrency
if
x>y-* m := x
a
y>x-± m := y
fi
Program 2.3
symbol -> separates the guard from the command, and the symbol • is
used to separate the component guarded commands from each other.
This solution clearly displays the inherent symmetry of the problem,
but at the cost of the non-determinism which is introduced.
Returning to the problem of the greatest common divisor, use
is made here of the repetitive construct to specify the looping within
the algorithm. This construct is delimited by the symbols do and od,
with the other symbols having the same meanings as before. The
repetitive construct operates (as the name would suggest) by cycling
around the component guarded commands, terminating only when all
the guards are false. Thus, as we see in program 2.4, the loop will
do
x>y-+ x:= x- y
D
y>x-* y:— y- x
od
Program 2.4
continue for as long as the values of x and y are different. The final
value of x (or y) is obviously the required value for the greatest
common divisor.
Comparing these guarded commands with the cobegin/coend
notation of the previous section, we can see that the non-determinacy
of the guarded commands is also present in the concurrent statement
notation, but without the control provided by the guards. Dijkstra
makes the point in his discussion of guarded commands thai the
commands which lie between if and fi, or between do and od, are an
unordered set of guarded commands, in the sense that the lexical order
Processes and the Specification of Concurrency 25
in which they are written is immaterial, and the same remark could be
made about the individual statements within the cobegin/coend
construction.
pid: = fork;
where pid is a variable which receives the identity of the newly created
process. It is not our intention to describe all of the subtleties of the
UNIX fork call, suffice it to say that the "cloned" process is an identical
copy of the code and data of the parent process with one exception.
That is that the value of pid only contains the identity of the newly
created process in the data space of the parent, whereas the value of
pid within the child process is zero. Using this knowledge, the process
can determine whether it is executing code in the child or in the
parent.
pid: = fork;
if pid = 0 then
begin
{this is code executed by the child process }
exec (...);
{ provided the exec call was successful, }
(this point is never reached, as this process }
{has now been overlayed by a new program }
end else
begin
{this is the parent}
cpid: — wait(s);
{ presumably cpid = pid at this point }
end
Program 2.5
The child will supply a status value when it reaches an exit
statement, and it may be desirable for the parent to discover this
status value as part of the resynchronisation. It may be necessary, for
instance, for the parent to know whether the child successfully
completed its task. This is the purpose of the parameter of the wait
system call, which provides a var parameter s to the wait call in which
the exit status may be returned. The returned value of the call itself is
the identifier of the process which has terminated. It can be seen from
this that if a parent has created several child processes, the wait call
will be satisfied, and the parent process allowed to continue, when any
of the child processes terminates, and the parent is able to discover the
identity of the terminating process by the value returned by the wait
call. If the parent needs to wait for a particular process to finish, then
it will have to issue several wait calls, until the result returned
indicates that the required process has indeed completed.
Summarising these two examples which employ fork and join,
we observe that:
(a) Both UNIX and Mesa will allow processes to be forked at any
point in the code, making it an extremely flexible method. By
the same token, they can execute join statements at (almost)
any time. The difference here is that since the Mesa JOIN must
specify the process to be joined, the process identifier must be
34 Processes and the Specification of Concurrency
in scope, and hence may limit the positions where the JOIN
may occur.
(b) UNIX requires that the code to be executed (which we assume
will normally be placed in the memory using an exec call)
should be loaded as a whole program, whereas Mesa regards
the code of a process to be of the same granularity as a
procedure. In fact, Mesa goes further and allows procedures
declared in the program to be called as procedures in the
ordinary way, or as processes, according to the wishes of the
programmer.
(c) Because of the requirement in Mesa that the program should
be strongly type-checkable, the JOIN statement is slightly less
flexible than the corresponding construct in UNIX. For most
examples, however, we would expect that both constructs
would be flexible enough.
(d) Although we are not at this point concerned with the way in
which independent processes interact with each other during
their respective executions, we may also observe here that the
Mesa model allows a forked process to access those variables
which were in existence at the time the FORK call was made,
and thus two (or more) processes can access the same
resources. By contrast, in the UNIX model, the variables which
had been declared at the time of the fork are copied to the child
process, and two instances of each variable are now in
existence, each process having access to only one of them.
Inter-process communication in the UNIX form of concurrency
is handled quite differently, as we shall see in chapter 5.
tos Data
Region
sfb'
machine executes Pascal statements taken from the code region, and
operates on program variables whose values are stored in the data
region. All variables are referred to by the program in terms of offsets
from one of two pointers into the data stack. Each procedure within the
Pascal program (including the main program) uses its own stack frame
within the data region to hold its parameters, its local variables and its
working space. When a new procedure is called, a new stack frame is
created on the top of the stack which contains, in addition to the local
data of the new procedure, linkage information allowing non-local
data to be accessed, and also the necessary information to allow the
stack to be restored to its former state on exit from the procedure. For
this particular architecture, we see that the state of a process running
on this hypothetical machine consists firstly of the two regions of
memory, and also of the three pointers into the memory shown in
figure 2.2, the program counter pc, (which of course indicates the
position in the code region where the next Pascal statement to be
executed will be found), a base pointer for the current stack frame sfb,
Processes and the Specification of Concurrency 39
2.8 Exercises
A<C,B<CyC<DyC<EyD<GyD<FyF<HyE<IyG<IyH<I
2.3 How does the directed graph of exercise 2.1, and the
corresponding concurrent program of exercise 2.2 change if the
following additional constraints are introduced?
(a) D<H.
(b) E<F.
(c) H<C.
cobegin
PI: count: — count + 1;
P2: count:— count + 1
coend
Program 3.1
machine(s) on which these concurrent statements are
to be run does not permit "atomic" incrementation of
variables in memory, and that the value must be
retrieved from memory to a register, incremented in a
register and then restored in memory, it will be
necessary to expand the concurrent program as shown
by program 3.2 where each of the statements
cobegin
PI: begin
regl := count;
regl :— regl + 1;
count: — regl
end;
P2: begin
reg2 := count;
reg2: = reg2 + 1;
count: = reg2
end
coend
Program 3.2
appearing within the begin blocks are taken to be
indivisible, and regl and reg2 are registers which may
be incremented, and are private to PI and P2
respectively. Under the arbitrary interleaving
assumption, we could conceivably find that these
statements are executed in the order suggested by
program 3.3.
Communication between Processes 45
begin
regl : == count;
regl : == regl + 1;
reg2: == count;
count: = regi;
reg2 : =: reg2 + 1;
count: = re#2
end
Program 3.3
cobegin
PI: begin
seek (20); read
end;
P2: begin
seek (88); write
end
coend
cobegin
PI: repeat
critical section 1;
non critical section 1
until false;
P2: repeat
critical section 2;
non critical section 2
until false
coend
Program 3.4
We assume that the two processes PI and P2 are both infinite
loops, each alternating between a critical section and a non-critical
section, an assumption which appears to be justified if the two
processes are part of an operating system providing a continuous
service to its users. The critical sections need not consist of the same
set of operations; they merely access the same shared resource. The
non-critical sections are considered to be totally independent of one
another.
There are three ground rules that must be obeyed by any
solution to the critical section problem:
(We have to make the assumption that no process stays in its critical
section indefinitely, as this would probably have a disastrous effect on
the whole set of processes. Rule 2 however admits the possibility of a
48 Communication between Processes
and:
{Solution 1}
var claiml, claim2: Boolean;
begin
claiml := false; claim2 := false;
cobegin
PI: repeat
repeat {do nothing} until not claim2;
claiml :— true;
critical section 1;
claiml := false;
non critical section 1
until false;
P2: repeat
repeat {do nothing} until not claiml;
claim2 := true;
critical section 2;
claim2 := false;
non critical section 2
until false
coend
end
Program 3.5
claim2 specifically to control the mutual exclusion of PI and P2 from
their critical sections, i.e. there will of course be the shared resources
which the critical sections themselves are designed to protect, and
claiml and claim2 are additional shared variables solely for the
purpose of implementing the critical section mechanism. They are
used to make claims on the use of the critical section on behalf of PI
50 Communication between Processes
{Solution 2}
var claiml, claim2: Boolean;
begin
claiml := false; claim2 := false;
cobegin
Pi: repeat
claiml := true;
repeat {do nothing} until not claim2;
critical section 1;
claiml := false;
non critical section 1
until false;
P2: repeat
claim2 := true;
repeat {do nothing } until not claiml;
critical section 2;
claim2 := false;
non critical section 2
until false
coend
end
Program 3.6
situation, suppose each process makes its claim before discovering
whether the other has a claim pending. Our revised solution is shown
as program 3.6.
As with the previous solution, the worst case occurs when the
two processes proceed perfectly synchronised. This time, however,
while we do ensure that the two processes are never in their critical
sections together, we can have the situation where each may be
Communication between Processes 51
{ Solution 3}
var claiml, claim2: Boolean;
begin
claiml := false; claim2 := false;
cobegin
PI: repeat
claiml : = true;
if claim2 then
begin
claiml := false;
repeat {do nothing} until not claim2;
claiml : — true
end;
critical section 1;
claiml : = false;
non critical section 1
until false;
P2: repeat
claim2: = true;
if claiml then
begin
claim2 : = false;
repeat { do nothing} until not claiml;
claim2 : = true
end;
critical section 2;
claim2 : = false;
non critical section 2
until false
coend
end
Program 3.7
waiting for the other to release its claim, neither will, and thus no
progress can be made at all.
This solution is unsatisfactory because each process makes a
claim, and then waits for the other to release its claim, which will not
happen because neither will complete its critical section. Suppose then
that we say that if, having made a claim, a process finds that the other
one has also made a claim, it withdraws its own claim until the other
process has completed its critical section and released its claim. Our
third solution now appears as program 3.7.
52 Communication between Processes
{Solution 4}
type process id = 1..2; {in this example }
var turn: process id;
begin
turn : = 1; {arbitrarily}
cobegin
PI: repeat
repeat { do nothing} until turn = 1;
critical section 1;
turn := 2;
non critical section 1
until false
P2: repeat
repeat {do nothing} until turn = 2;
critical section 2;
turn := 1;
non critical section 2
until false
coend
end
Program 3.8
Program 3.8 will certainly ensure that the two critical sections
are not entered simultaneously, and neither will either of them be
delayed indefinitely waiting for the other to change the value of turn,
but here we are violating ground rule number 3. This solution causes
the two critical sections to be executed strictly alternately, and hence
if PI (say) was proceeding quickly enough to try to enter its critical
section more frequently than P2, then PI would be slowed down to the
Communication between Processes 53
{ Dekker's solution}
type process id = 1..2;
var claiml, claim2: Boolean', turn: process id;
begin
claiml : = false; claim2 : = false; turn : = 1;
cobegin
PI: repeat
claiml := £rae;
while claim2 do
begin
claiml : — false;
repeat {do nothing} until turn = 1;
claiml :— true
end;
critical section 1;
claiml := /h/se; furrc := 2;
non critical section 1
until false;
P2: repeat
claim2 := £rue;
while claiml do
begin
claim2 := false;
repeat {do nothing} until turn — 2;
claim2 : = true
end;
critical section 2;
claim2 : = false; turn : = 1;
non critical section 2
until false
coend
end
Program 3.9
to Peterson is presented as program 3.10. It is interesting, however, to
observe that this pair of concurrent processes requires only the
memory interlock to allow the method to work, and that a minor
increase in hardware assistance will give rise to a considerable
reduction in complexity.
Communication between Processes 55
{ Peterson's solution}
type process id = 1..2;
var claiml, claim2: Boolean; turn: process id;
begin
claiml := false; claim2 := /a/se;
cobegin
PI: repeat
claiml := £rue;
turn:- 2;
repeat {do nothing} until
not claim2 or turn — 1;
critical section 1;
claiml :— false;
non critical section 1
until false;
P2: repeat
claim2 := true;
turn := 1;
repeat { do nothing} until
not claiml or turn — 2;
critical section 2;
claim2 := false;
non critical section 2
until false
coend
end
Program 3.10
and:
the scheduler recomputes the deadline for that task using the interval
for that particular process.
In this example, the semaphore mechanisms are being used
purely for the purposes of passing timing information between
concurrent activities, so that the scheduler can receive basic timing
information from the timer, and disseminate it as appropriate amongst
the tasks being scheduled.
repeat
"produce message";
"wait until message is consumed"
until false
repeat
"wait until message is produced";
"consume message"
until false
Figure 3.1.
Text
Generator
Producer Consumer
varp, c: semaphore;
begin
p : = false; c : = false;
cobegin
Producer: repeat
produce message;
signal (p); { message produced }
wai£ (c)
until false;
Consumer: repeat
wait(p);
consume message;
signal (c) { message consumed }
until false
coend
end
Program 3.14
subsequently delayed, the consumer is able to occupy its time usefully
in "clearing the backlog" of messages waiting in the buffer. Of course,
if there truly are no messages for the consumer (i.e. the buffer is
empty) then the consumer must wait because there will be nothing for
it to do. It is also the case that no practical implementation of a
message buffer could afford to allow the producer to generate an
indefinite number of messages without the consumer taking some out,
that is we could expect only a finite number of messages to be buffered.
This problem is known as the bounded buffer problem. Clearly it is
necessary to prevent the producer from generating messages when the
Figure 3.2.
Producer Consumer
Communication between Processes 65
buffer is "full". However, despite the necessity for both producer and
consumer to be delayed occasionally, we would certainly expect a
buffered message system to smooth out local variations in the speeds of
both processes, so that overall the performance (i.e. the time to pass
some arbitrary number of messages) would be better than with the
unbuffered system. Using these notions of a buffered message passing
scheme, program 3.15 describes the actions of the producer and
consumer processes.
cobegin
Producer: repeat
"produce message";
if"buffer is full" then
"wait until buffer not full";
"place message in buffer"
until false;
Consumer: repeat
if "buffer is empty" then
"wait until buffer not empty";
"retrieve message from buffer";
"consume message"
until false
coend
Program 3.15
In the same way as we used semaphores in the unbuffered
message case, so we can use semaphores to indicate changes in the
"fullness" or "emptiness" of the buffer. The producer will have to wait
if a semaphore indicates that the buffer is completely full, and the only
way that the buffer will cease to be full is by the consumer taking a
message out. Thus the consumer can be responsible for signalling to
the waiting producer process. In a precisely symmetric way, the
consumer must wait if a semaphore indicates that the whole buffer is
empty, and the producer can signal if it puts a message into the buffer.
By introducing the idea of a buffer, to which both the producer
and consumer have access, we have added an additional complication
to the problem, in that the use of a shared resource has now become
explicit. In program 3.14, the mechanism by which a message passed
from producer to consumer was not defined precisely, and the implicit
66 Communication between Processes
assumption was made that the underlying mechanism would carry out
this task correctly. In program 3.16, which shows a solution including
all the implementation details, the accesses to the buffer have now
become explicit, which implies that the mutual exclusion required to
prevent undesirable interference must also be made explicit.
be duplicating the test that has already been made. However, the test
for the number of messages in the buffer and the wait operation can
not be made indivisible, and therefore there could be interleavings
which might cause the program to behave incorrectly.
and then the amended definitions of wait and signal would be:
68 Communication between Processes
Program 3.17
Communication between Processes 69
and:
constBufferCapacity = ...;
var mutex, full, empty: semaphore;
begin
mutex: = 1; full: — 0; empty : = BufferCapacity;
cobegin
Producer: repeat
produce message;
wait (empty); {wait for an empty slot}
wait (mutex);
place message in buffer;
signal (full);
signal (mutex)
until false;
Consumer: repeat
wait (full);
{wait for at least one message }
wait (mutex);
retrieve message from buffer;
signal (empty);
signal (mutex);
consume message
until false
coend
end
Program 3.18
the integer-valued semaphore to keep a count of the number of
messages in the buffer, while another indicates how many empty
places are still left in the buffer. Program 3.18 gives an alternative
concurrent program making use of these additional facilities.
We have seen several examples of concurrent programs in this
chapter, including a number of different ways in which communication
and co-operation between independent processes running in parallel
can be achieved. However, the use of semaphores is only one method of
achieving this end. In the following chapter, we shall examine some
ways of providing more structured constructs for inter-process
communication.
Communication between Processes 71
3.10 Exercises
3.2 Repeat exercise 3.1 for the example in which a shared variable
is incremented. (Note that there are now twenty possible
interleavings.)
Program 3.19
Communication between Processes 73
3.9 Show that the TestAndSet method could give rise to starvation,
and explain why this is unlikely to happen in practice.
Program 3.20
Communication between Processes 75
writing back the new value, and this would also increase the
probability of interference occurring.
with the value false (instead of true) in program 3.12, then both PI and
P2 would be delayed indefinitely waiting for access to their respective
critical sections.
type T = ...;
var v: shared T;
begin
initialise (v);
cobegin
PI: repeat
region v do critical section 1;
non critical section 1
until false;
P2: repeat
region v do critical section 2;
non critical section 2
until false
coend
end
Program 4.3
82 Shared Data
Px: begin
region v do
begin
await B(v);
end;
end
Program 4.4
region. The reason for this is not hard to find, and relates to the fact
that if a critical section wishes to act upon some shared data, it is
likely to check to see whether the data is in a suitable state (i.e. some
predicate applied to the data is true) before it begins to operate on the
data.
The question of when the await statement is used raises an
important issue in the use of conditional critical regions, and indeed,
in consideration of critical sections in general. We have assumed that
the shared data structure which is to be protected must have some
property which is always true; a consistency condition. This
consistency condition is sometimes known as an invariant. The critical
section is necessary because at some point during the manipulation of
the data structure, the invariant is not true. We make the assumption
that critical sections are well-behaved in the sense that the data
structure, while it may be temporarily inconsistent during the
execution of the critical section, always satisfies its invariant outside
the critical section. Thus the critical section will always return the
data structure to a consistent state on exit.
The await statement clearly has a similar responsiblity. The
critical region is left on encountering an await statement with a false
Boolean, and thus other processes may be allowed access to their
critical regions and hence to the shared data. Such processes will
clearly expect the invariant to be true of the data on entry to their
critical regions, and hence processes using await must ensure that the
84 Shared Data
4.3 Monitors
The critical region and its extended form, the conditional
critical region, will provide most of the facilities which are required
from a synchronisation and communication mechanism for co-
operating concurrent processes, although it is not easy to see how the
final example of Chapter 3, the scheduling of tasks, could be
implemented using these constructs. Beyond the ability to check that
shared data is not accessed outside a critical region, however, they do
not apply any discipline to the way in which the shared data is
manipulated. That is, once approval has been obtained for access to the
shared data, there are no constraints on the operations which may be
performed on that data. There is, therefore, an obligation on the
programmer to ensure that the manipulations carried out on the data
structure during the execution of the critical region do not leave the
structure in an inconsistent state, i.e. the invariant is true on exit from
the critical region.
To impose some controls over the way in which a shared data
structure is manipulated, the next synchronisation construct we shall
consider is an extension of the notion of class as found in the
programming language Simula67. Simula67 is not a concurrent
language, but some of the facilities it provides go some way to allowing
the implementation of controlled communication between concurrent
processes.
The Simula class allows the programmer to manipulate a data
structure in a controlled way by making access to the data impossible
except through a defined interface, consisting of a set of functions
and/or procedures. Thus we can ensure that unconstrained
interference with the data is impossible, since the creator of the class
can define those, and only those, operations on the data which do not
destroy the consistency of the data structure. Another feature of the
class construct is that it encourages the use of abstract data types upon
which operations can be defined without the client of these operations
being burdened with the implementation details of the data type.
By compelling the user of the data structure to access it only
through the defined operations, the author of a class can ensure that
Shared Data 85
the concrete representation of the data (i.e. the local variables of the
class) is in a consistent state on exit from the class. Furthermore, we
can use the principle of the class to group together data and operations
in a single structure. This property of the class is precisely what is
required to maintain the integrity of a data structure when it is
desired to operate upon it with a number of concurrent processes. The
only difference between the Simula class and the shared data
considered earlier is that Simula is a sequential language, and so does
not need to worry about possible interference while the procedures and
functions of a class are being executed.
Suppose then that a structure such as the Simula class were
used to provide a shared abstract data object. The discussion above
suggests that the consistency of the concrete data might be
compromised if a process could access the data while another operation
is in progress. The monitor construct, proposed by Hoare, provides the
notion of a shared class, but goes on to insist that any process wishing
to execute an operation of the class may do so only if no other process is
currently accessing the data. If then we imagine our class as consisting
of a set of (hidden) variables together with a set of (visible) procedures
and functions, but with the additional restriction that only one process
may be executing any of the procedures or functions at a time, then we
have the synchronisation/communication mechanism known as a
monitor.
In some ways the monitor is not unlike the critical region in
that access to the components of the shared variable is only permitted
using particular sections of the code. The principal difference is that
the monitor provides a single object in which the data and the
operations on that data are collected together in one place within the
program. In other words, it is not necessary to broadcast all the details
of the data structure to all the processes which might wish to use it,
but merely provide operations (with appropriate parameters) which
will manipulate the data structure on behalf of the processes. In this
sense, it is the data structuring aspects - the so-called "information
hiding" property - of the monitor which distinguishes it from the
critical region.
Now as with the critical region, so with the monitor; there is a
logical problem associated with giving exclusive rights of access to a
data structure to a process which may be unable to use it for other
86 Shared Data
SingleResource:
monitor
begin
var busy: Boolean;
NonBusy: condition;
procedure acquire;
begin
if busy then NonBusy .wait;
busy : = true
end; { acquire}
procedure release;
begin
busy := false;
NonBusy .signal
end; {release}
{ monitor initialisation}
busy: — false
end; { monitor SingleResource}
Program 4.5
operations performed on the condition variable are the wait in acquire,
and the signal in release. In the monitor SingleResource, as in all other
instances, the condition variable is associated with some predicate of
the data structure which can be either true or false. In this simple case,
the condition NonBusy is directly associated with the falsity or truth of
the Boolean variable busy.
It is important to be aware of the sequence of events which
takes place when a wait or a signal is invoked, and the constraints on
where these operations may be used. When a wait is performed on a
condition, the effect is to send the calling process to sleep, thus
allowing another process to have access to the monitor. It is obvious
that this should not be allowed to happen when the data of the monitor
is in an inconsistent state. In this simple case, it is rather difficult for
the monitor data to be inconsistent, but in more complicated
situations, such as we shall encounter later, this is an important
consideration.
Another point to notice about the way in which wait is used is
that it appears within the control of an if statement, i.e. the Boolean
expression is tested once, and if the result is found to be true, the wait
is executed. The implication of this is that when the process is awoken
88 Shared Data
MonitorName:
monitor
begin
{declaration of local variables}
{including conditions}
{declarations of local and}
{ visible procedures/functions }
{initialisation statements}
end; {monitor}
Program 4.6
process (i.e. the process which made the signal call) should leave the
monitor immediately after the condition is signalled, in order to
prevent the situation in which this process awakens a waiting process,
and then itself alters the data and thereby re-imposing the
circumstances which caused the signalled process to wait in the first
place.
It is important to emphasise that, although the behaviour of a
monitor condition variable would appear to be similar to that of a
semaphore (and we may have enhanced this impression by using the
same names for the operations acting upon them), there are two very
significant differences between them. Firstly, the wait operation on a
condition variable will always cause the calling process to suspend
itself, unlike the semaphore wait which will decrement the semaphore
counter and then only wait if the resulting value is negative. Thus,
Shared Data 89
up of the local variables can be done before any process attempts to use
the monitor.
buffer:
monitor
begin
const buffsize = ...;
{ some appropriate value }
var b: array [O..buffsize-1] of item;
p, c: O..buffsize-1;
n: 0..buffsize;
notfull, notempty: condition;
procedure get (var m: item);
begin
if n — 0 then notempty.wait;
m:- b[c\;
c:= (c + 1) mod buffsize;
n:~ n- 1;
notfulLsignal
end; {get}
procedureput(m: item);
begin
ifn = buffsize then notfull.wait;
b[p]: = m;
p := (p + 1) mod buffsize;
n := n + 1;
notempty.signal
end; {put}
{initialisation code }
p:= 0;c:=0;n:= 0
end; { monitor buffer}
Program 4.7
with each of these two states, we then have definitions for all of the
local variables of the monitor.
It is unnecessary to describe the code in great detail, but one or
two points need to be made. Firstly, the variable n will at all times
contain the value of the number of full positions in the buffer, except of
course when either of the procedures get or put is being executed.
Assuming that the buffer contains both empty and full positions, the
variables p and c represent, respectively, the first empty position and
the first full position in the buffer. We observe here that, as a
consequence of this particular method of implementation, there are
two degenerate cases where p and c point at the same buffer slot. These
two cases may be distinguished from one another by inspecting the
92 Shared Data
value of n. Again, this statement is only true when the procedures are
not actually in execution. These observations about the interpretation
of the variables p, c and n represent consistency information
concerning the state of the buffer, and the fact that they may not be
true during the execution of the procedures is an indication of the
necessity of the buffer procedures being critical sections of code, and
hence being encapsulated within the monitor. The observation might
also be made that the value of n can never go outside its stated bounds
(O..buffsize)> since within get, it can only be decremented if the
conditional statement
if n = 0 then nonempty.wait
has been passed, which in turn can only happen if either the value of n
was already greater than 0, or is signalled from the procedure put
immediately following the incrementation of n. By a symmetric
argument, n can never exceed the value buffsize.
The initialisation code of the monitor simply consists of the
assignment of the value zero to n, indicating that the initial state of
the buffer is empty, and initially the two pointers p and c have to point
to the same buffer slot, slot zero being a convenient, but not essential,
position to start.
The bounded buffer problem discussed in this section provides
a simple example of the use of monitors, showing not only the necessity
for mutual exclusion from critical parts of the code, but also the
facilities available for processes to signal between themselves to
indicate certain facts about the state of the shared data structure.
Although it has more to do with the encapsulation of the data than the
concurrency aspects per se, we may also observe that the user of the
buffer is not required to know the details of the implementation of the
buffer. It is sufficient for the monitor to advertise the routines which it
makes available (and their purpose). If it were felt appropriate to alter
the implementation of the bounded buffer, provided the externally
visible procedures were not altered in appearance this could be done
without alteration to (or even knowledge of) the client of the buffer.
Shared Data 93
ConditionName. queue
Shared Data 95
which returns the value true if there is at least one process waiting,
and false otherwise.
ReadersA ndWriters:
monitor
begin
var readercount : integer;
writing : Boolean;
oktoRead,
oktoWrite : condition;
procedure RequestRead;
begin
if writing or oktoWrite.queue then
oktoRead.wait;
readercount := readercount + 1;
oktoRead.signal
end; {RequestRead}
procedure ReleaseRead;
begin
readercount : = readercount- 1;
if readercount = Othen
o££o Write.
end; {ReleaseRead}
procedure RequestWrite;
begin
if writing or (readercount < > 0) then
oktoWrite.wait;
writing: = true
end; {RequestWrite}
procedure Release Write;
begin
writing: = /a/se;
if oktoRead.queue then
oktoRead.signal
else oktoWrite.signal
end; {Release Write }
{initialisation }
readercount: — 0; writing : — false
end; { monitor ReadersAndWriters}
Program 4.8
The solution to the readers and writers problem is now given
as program 4.8 in which we see the four visible procedures together
with the local variables discussed earlier. Taking the most
96 Shared Data
DiskScheduler:
monitor
begin
type cylinder — O..MaxCyl;
var headpos: cylinder;
direction : (up, down);
busy : Boolean;
upsweep, downsweep : condition;
procedure request (dest: cylinder);
begin
if busy then
begin
[{(headpos < dest) or
((headpos = dest)
and (direction = up)) then
upsweep.wait (dest)
else downsweep.wait (MaxCyl - dest)
end;
6*zsv : = true; headpos : = dest
end; {request}
procedure release;
begin
6 ^ /
[{direction = ap then
begin
if upsweep.queue then
upsweep.signal else
begin
direction : = down;
downsweep.signal
end
end else
if downsweep.queue then
downsweep.signal else
begin
direction : = up;
upsweep .signal
end
end; {release}
headpos : = 0; direction : = up; busy : =
end; { monitor DiskScheduler}
Program 4.9
100 Shared Data
begin
M:
monitor
begin
varp, q : condition;
procedure stop;
begin
p.wait;
q.signal
end; {stop}
procedure go;
begin
p.signal;
q.wait
end; {go}
{initialisation code}
end; {monitor M}
cobegin
PI: begin
repeat
M.stop;
until false
end; {PI}
P2: begin
repeat
M.go;
until false
end; {P2}
coend
end
Program 4.10
102 Shared Data
M:
monitor
begin
var p, q : condition;
psignalled,
qsignalled : Boolean;
procedure stop;
begin
if not psignalled then p. wait;
psignalled: = false;
qsignalled: = true;
q.signal
end; { stop}
procedure^;
begin
psignalled: = true;
p.signal;
if not qsignalled then q.wait;
qsignalled: = false
end; {go}
{initialisation code }
psignalled: = false;
qsignalled: — false
end; {monitor M}
Program 4.11
Shared Data 103
path a end
This simply means that the data object permits only one operation, a,
which may be executed at any time, and to any desired level of
concurrency. Similarly, no concurrency control is implied by the path:
path 2: a end
Shared Data 105
path a; b end
an empty buffer. In the monitor example given in section 4.4, these two
situations were represented by two condition variables within the
monitor.
The view taken by the path object is that an attempt to remove
an item from an empty buffer cannot occur if the path insists that a get
operation must be preceded by a put, and that it is impossible for the
full buffer problem to arise if operations on the buffer occur as put/get
pairs, and that a maximum of MaxBuff such sequences are in progress
at any time. Thus the path
or:
In the first case, a process is not permitted to read until an acquire has
been completed, and a release must be done following the read to
complete the path and make it possible for a further acquire to take
place. Similarly, in the second case, either a read or a write may be
executed (but only one of them) provided an acquire has been executed
previously, and a release must follow the reading or writing.
We should note that although this path mechanism prevents
undesirable occurrences such as the use of the resource without its
prior acquisition, it does not prevent a rogue process from using the
resource when it has been acquired by another process. In order to
prevent this type of fraudulent use of the resource, it is necessary for
the implementation of the resource object to take note of the identity of
the process which has successfully acquired it, and to allow only that
process to make use of, and to release the resource.
In addition to the paths and path constructors described so far,
there is an additional constructor which is particularly useful in the
context of the Readers and Writers problem (see section 4.4.2). This is
the so-called "burst" mode. An operation (or combination of operations)
may be enclosed in square brackets as:
108 Shared Data
[a]
4.7 Exercises
Figure 4.2
and:
Figure 5.1
Message Passing 117
acting both as a client and as a server at the same time. Suppose now
that P3, in processing a request from PI, sends a message to P4. It will
be unable to complete the service for PI until a reply is received from
P4y and would therefore call the receive procedure. If this procedure
simply accepted the next message to be sent to P3, then the message
received might be coming from anywhere such as a new client P2. If
this occurred, P3 would have to remember the message from P2 and
process it at a later stage, or else send a reply to P2 asking for the
message to be resubmitted later.
reply is received are illustrated in figure 5.2(a) and (b). In both of these
diagrams, we have shown the receive being executed before the send,
i.e. the receiving process has to wait until the send primitive is called
by the sending process. The situation in which the send occurs before
the receive is illustrated in figure 5.3(a) and (b). Notice that in figure
5.3(b), the sending process is delayed for two reasons; firstly because
the receiving process is not ready to accept the message, and then
because the reply has to be returned before the sender can continue.
The first of these delays would be less likely to occur if the message
system included a message buffer.
Sender - Send i
i
Receiver - 1L
Receive
Sender - Send
Receiver - Receive
i i
!
Receiver " *-
Receive Reply
order to ascertain the identity of the process to which the real request
should be sent. As an example, suppose a client process wishes to make
use of services offered by a data base server DB. The client would not
have necessarily to remember the identity of DB, but could send a
request to the name server (NS) which says "please tell me the identity
of the data base server". The name server may now reply "the data
base server you require can be accessed using the identity DB". The
client is now able to communicate with DB without any further
interaction with NS.
This approach has a number of advantages. Firstly, there may
be good reasons why the system administrator may wish to change the
identity of a particular process. If clients observe the protocol given
above in order to access the services they require, then the system
administrator is free to alter the identity of any process, and the only
change required is one adjustment to the information stored in the
name server. The alternative is that all processes wishing to access
another service would have the identity of that service bound in, and
all such processes would require modification if the identity of the
service were to be changed. There may, of course, be transient
situations in which a process has asked the name server for the
identity of a server process, and during the lifetime of the subsequent
interaction with that server, its identity may have to be changed. In
such cases, it would be necessary to intercept all subsequent references
to the server process during the interaction, and request that the client
enquire again of the name server for the new identity of the server.
Another advantage of the name server approach is when the
service could be provided by any one of a number of (nearly) identical
servers. For instance, suppose a client wished to send information to a
printer, and suppose also that there were several printers available,
each with its own driver process. The client may not be concerned
which of the printers is to be used to satisfy his request, so that an
additional function of the name server could be to provide the identity
of one of a number of possible servers, according to which one of them
was available. Hence, the client sends a message to the name server
asking "please tell me the identity of a (any) printing server" to which
the name server might reply "use PS2"y knowing that PS2 is free,
whereas PSO and PS1 may not be. The name server may have this
knowledge stored in its own tables, implying that all use, and indeed
Message Passi ng 121
transferred have been accepted by the pipe, and this may mean that
the writing process is delayed until another process has removed some
bytes. The exception to this behaviour is when a write request asks to
transfer more bytes than the maximum capacity of the pipe, in which
case, the write request succeeds, but returns a value equal to the total
number of bytes actually transferred.
how other processes behave in this case. Let us consider first what
happens if the number of writers becomes zero. All outstanding read
requests (i.e. processes sleeping waiting for data to become available)
are awoken, and the read calls returned with an indication that no
data has been read. (This is equivalent to the end-of-file indication
when reading from a file.) All subsequent read attempts also return
with zero bytes having been transferred. If, as a result of a close
request, the number of readers reduces to zero, then any process
waiting to write to the pipe is awoken, and a UNIX signal, or exception,
is sent to the process. This would be a somewhat unusual situation,
since the fact that a write call puts the process to sleep means that the
pipe is full, and yet the last reader has closed the pipe.
5.5.4 Sockets
The BSD4 series of UNIX systems, written by the University of
California at Berkeley, adopted a different approach to the problem of
inter-process communication, in which it was recognised that it would
be useful to be able to communicate not only with processes within the
same system, but also with processes in other UNIX (and possibly non-
UNIX) systems, running on different machines, and connected by a
communications network.
It is not our intention here to discuss the problems of
communicating over networks, but to describe how these facilities may
be used for inter-process communication within a single system.
BSD UNIX offers the programmer a system call named socket,
which essentially creates an "opening" through which communication
may take place. In making a socket call, the process must specify a
domain within which this inter-communication will take place. This is
primarily used to specify a family of communication protocols to be
used over the network, but one of the options is the UNIX domain.
Having created a socket, the process mu^t then bind the socket to an
address, the form of which depends on the domain to be used. In the
case of the UNIX domain, this address has the form of a UNIX filename.
A process wishing to communicate with another process will
make the system call connect, specifying the address of the intended
correspondent. This call will clearly fail if the specified address does
not exist, or if no process is ready to receive a connection request
naming this address. A server process will indicate its willingness to
establish connections by issuing the call listen on a socket which has
already been opened and bound to a well-known address. Processes
wishing to avail themselves of the facilities of the server must then
128 Message Passing
connect their own socket to this well-known address. The listen call
returns when a connection request is received, and will then call the
function accept. This will return with a new socket descriptor which
will then be used for the conversation with the client, whose connect
call will return when the conversation is properly set up. The creation
by the server of a new socket as a result of the accept call allows the
server to fork a new process to handle the client's request, and to
return itself to its own main loop, where it can listen on the original
socket for further service requests.
Once the conversation has been established, the data
transferred is once again simply a sequence of bytes, and hence
standard read and write system calls can be used. The socket
mechanism also has a send and a recv system call, and these may take
an additional parameter in order to take advantage of certain other
facilities offered by sockets. These are primarily concerned with
network communications, and as such are beyond the scope of this
book.
civ or cle
and either (but not both) of the components of a guard may be empty,
and if the < Boolean expression > is not present, a true value is
assumed.
The two <i/o guard > constructs represent communication
between processes in CSP, and the notion of information passing into
or out of a process from or to its environment suggests that
input/output is an appropriate description of these constructs. The
construct civ describes the operation of accepting a value on channel c,
and assigning that value to the variable v. The construct cle causes the
evaluation of the expression e, the result of which is delivered on the
channel c. Neither of these two constructs can complete until another
parallel process is ready to match up with the input/output operation
requested. Thus, the i/o guard civ will remain pending until there is a
parallel process attempting to perform an output operation specifying
the same channel c.
The value of the guard is then:
cobegin
buffer: repeat
until false;
producer: repeat
producemessage (ra);
send (buffer, "put", ra);
receive (p, ra)
until false;
consumer: repeat
send {buffer/'get");
receive (p, ra);
consumemessage (ra)
until false;
coend
Program 5.2
We show the operation of the buffer manager process in
program 5.3. This process (like the other two) is in a continuous non-
terminating loop, and begins by waiting for a message. When one
arrives, it must first determine which function it is to carry out. In this
simple case, it will either be a put or a get.
Consider a request (by the producer) to put a message mess into
the buffer. The buffer manager has first to decide whether there is any
space in the buffer to accept the message. If not, then the message is
saved in the local variable savemess, and the identity of the requesting
process is saved in the variable putprocess. The purpose of this
variable, and of the variable getprocess, is to remember the identity of
the process whose request has had to be deferred because certain
conditions on the data controlled by the manager are not met. In this
respect, these variables correspond to the condition variables of the
original monitor. The simplifying assumption that there is only one
producer and one consumer in the system means that a single variable
is sufficient for each condition in this particular case. In general, these
variables would have to be capable of remembering a number of
processes, and would therefore have to be capable of representing a
queue of processes. In the case of a full buffer in the multiple producer
134 Message Passing
buffer: repeat
receive (pidy request, mess);
if request = " p a r then
if n — buffsize then
begin {buffer is full}
sauemess: = mess;
putprocess: = pid
end else
begin
b [p]: = mess;
send (pid, nullmessage);
p : = (p + 1) mod buffsize;
n:=n+l;
if getprocess < > nullprocess then
begin
mess := 6 [c];
send {getprocess, mess);
c := (c + 1) mod buffsize;
n : = n - 1;
getprocess: = nullprocess
end
end
else
if request = " g e f t h e n
if n = 0 then
{ buffer is empty}
getprocess: = pid
else
begin
send {pid, b[c\);
c:— (c + 1) mod buffsize;
n:= n-1;
if putprocess < > nullprocess then
begin
send {putprocess, nullmessage);
b [p]: — savemess;
p := (p + 1) mod buffsize;
n := n + 1;
putprocess := nullprocess
end
end
until /a/se;
Program 5.3
Message Passing 135
this we can see that the "active buffer" approach is able to exploit
parallelism to a greater extent than can be achieved using the monitor
mechanism, in which all the housekeeping tasks of the buffer have to
be performed within the monitor procedures, which preclude the
continuation of the calling processes while these tasks are being
carried out.
5.8 Exercises
5.1 A message-passing system is described as synchronous if the
send and the receive must be executed simultaneously for a
message to be passed, and asynchronous if a buffering scheme
allows the possibility of the send being completed some time
before the receive is executed. In a system in which only the
send and receive operations are available, explain why the
distinction between synchronous and asynchronous message-
passing is invisible to the communicating processes.
5.2 A server process is frequently coded in the following form:
program ServerProcess;
var client : Processld;
mess, rep : Message;
begin
initialisation;
repeat
client := receive (m);
MainBody;
{ This code is dependent on the content
of mess and maybe client. }
send {client, rep) { probably }
until false
end. { ServerProcess }
type Producer —
process
var mess: Message;
begin
cycle
produce (mess);
buff,send {mess)
end {cycle}
end; { Producer }
type Consumer —
process
var mess: Message;
begin
cycle
buffreceive (mess);
consume (mess)
end {cycle}
end; { Consumer }
Program 6.1
Using the bounded buffer example to illustrate the use of
Concurrent Pascal, program 6.1 shows the process declarations for the
producer and consumer. In these two processes we assume that:
(a) there is a declaration for the type Message appropriate to the
application.
and
(b) there is a monitor called buff which implements the bounded
buffer.
The produce call in the producer and the consume call in the
consumer are intended to represent the purely local operations
concerned with the producing and consuming of each message,
respectively. We therefore assume that they require no further
elaboration. We note also the use of the construct cycle in both
processes. Sequential Pascal will allow us to write
142 Languages for Concurrency
or
cycle... end;
type BoundedBuffer =
monitor
const nmax = ...;{ some suitable value }
var n: O..nmax;
p, c: O..nmax-1;
full, empty: queue;
slots: array [O..nmax-1] of Message;
Program 6.2
Languages for Concurrency 145
use the buffer, what to do when that number is exceeded, and how to
schedule the restarting of processes following a continue operation.
To return to the example, we have already shown the
necessary type definitions to set up the two processes and the monitor,
so that all that is now required is the main program in which these
definitions occur. As before, in addition to the type definitions, we
require a declaration of each of the entities and an init statement to
start them all going, as shown in program 6.3.
begin
init buff, prod (buff), cons (buff)
end.
Program 6.4
6.3 Mesa
The programming language Mesa has been in use for some
years internally within the Xerox Corporation, before it was finally
published in the open literature. It was designed primarily as a
systems programming language, and although it is possible to detect
indications of its ancestry, by and large it is a language unlike other
languages of its generation. Its primary goals are to provide a very
type-safe language, even across the boundaries between separately
compiled sections of program. Mesa has the concept of a module, whose
function is to provide the ability to partition a program into discrete
portions. In particular, Mesa provides an elegant mechanism for
Languages for Concurrency 149
defining interfaces, to which both client and server must conform, thus
providing a type-checkable connection between separate sections of
code. The concurrency features of the language are in keeping with the
current ideas at the time the language was designed, and constitute a
very flexible set of facilities.
As we saw in chapter 2, new processes in Mesa are created by
the use of the FORK operation, which takes a procedure as its
argument, and returns a value of type PROCESS as its result. The
parent process may resynchronise with the child process thus created
by using the JOIN operation, which also provides the opportunity for
results to be returned by a child process to its parent. We also saw how
this mechanism provided type safety.
Processes in Mesa may require communication between one
another beyond the points of creation and termination of the child.
This is provided by a variant of the monitor mechanism. Mesa already
provides a module structure, which provides all of the desired facilities
except for mutual exclusion, and therefore, as we have already seen in
Concurrent Euclid, it is simply a matter of allowing the word
PROGRAM (the word used in Mesa to mean module) to be replaced by
the word MONITOR without making any other changes to the
structure, and all of the desired properties are available. There are a
number of additional features associated with the Mesa MONITOR
construct, many of which are beyond the scope of this section (for
details refer to the Mesa Language documentation), but there is one
detail which we will mention here. Within a MONITOR, it is possible to
declare procedures which will have one of the three attributes ENTRY,
INTERNAL or EXTERNAL. Procedures designated as being either
ENTRY or INTERNAL are simply those procedures which are or are not
available to processes outside the monitor. ENTRY procedures claim
exclusive access to the data of the monitor, and may use INTERNAL
procedures to carry out the desired operation. The attribute
EXTERNAL is used to indicate that the procedure may be called from
outside the monitor, but unlike an ENTRY procedure, it does not claim
exclusive access to the data of the monitor. This feature is useful if
there are operations on the monitor data which can safely be
performed without exclusive access, but which for reasons of good
structuring, should be included in the module comprising the monitor.
150 Languages for Concurrency
c : CONDITION;
WAIT c;
and:
NOTIFY c; or BROADCAST c;
ProdCons: PROGRAM =
BEGIN
Producer: PROCEDURE =
BEGIN
WHILE true DO
ProduceMessage [m];
Buffer.Put [m];
ENDLOOP;
END;
Consumer: PROCEDURE =
BEGIN
WHILE true DO
ProduceMessage [m];
Buffer.Put [m];
ENDLOOP;
END;
END
Program 6.5
program has been omitted, in order not to obscure the main point of
this example.
Languages for Concurrency 153
BBuffer: MONITOR =
BEGIN
END
P r o g r a m 6.6
put operation until enough get operations have been performed that
there is a least one available message position in the buffer.
What these two condition variables are really ensuring is that
for each message which is presented to the buffer, a put is followed by a
get, and that only a limited number of put/get pairs may be in progress
at any one time. Using path expressions, it is possible to ensure that
these constraints are satisfied, without the use of condition variables
and their corresponding signals and waits (or delays and continues).
Suppose we have a bounded buffer of (constant) size N. This
means that we must ensure that a get operation is not started until its
corresponding put is complete, but that up to N puts which have no
corresponding ge£ may be executed. The following path expression will
provide these constraints:
process producer,
var msg: message;
begin
repeat
produce (msg);
buffer.put {msg)
until false
end;
process consumer;
var msg: message;
begin
repeat
buffer.get (msg);
consume (msg)
until false
end;
procedureput(m: message);
begin
sZote [p]: = m;
p:= (p + l)mod 10;
end;
begin {initialisation }
p:= 0;c:= 0
end; { object buffer }
Program 6.8
We now return to the question of the variablerc,the number of
occupied buffer slots. As we have observed, the straightforward
bounded buffer problem can be solved in Path Pascal without any
explicit reference to this value, but we may extend the problem
slightly to require that a process may wish to discover how many items
there are in the buffer. In other words, the buffer object may be
required to offer a function which will return the value of the number
of occupied slots. Such a requirement would necessitate the explicit
appearance of the variable n. The consequence of this is that both put
and get would need to alter the value of n, and hence a potential point
of interference is created.
Fortunately, Path Pascal permits the use of nested objects, and
therefore of nested paths. Using this, it is possible to create an object
within the buffer object, whose sole function is to control access to the
Languages for Concurrency 159
procedure incn;
begin
n \— n + 1;
end;
procedure decn;
begin
n := n-1;
end;
begin {initialisation}
n:= 0
end; {object count}
Program 6.9(a)
Program 6.9(a) shows the object count which includes the path
defined above, and program 6.9(b) shows the new version of the object
buffer, including the extension to the functionality whereby a process
may interrogate the number of full buffer slots.
160 Languages for Concurrency
end;
procedureput(m: message);
begin
s/ote [p]: = m;
p:= (p + l)mod 10;
count.incn
end;
begin {initialisation}
p : = 0;c:= 0
end; {object buffer}
Program 6.9(b)
6.5 ADA
In all the previous examples in various languages, the
implementation of the bounded buffer has employed a passive
structure to act as the buffer itself, including the operations needed to
Languages for Concurrency 161
extremely unlikely that they will. Thus one or other of the processes
must wait for the rendezvous to take place. When the two processes do
meet, the code of the entry is executed "jointly" by the two processes,
after which they part company and go their separate ways.
Diagrammatically, we may illustrate the two situations by figures
6.1(a) and (b). In figure 6.1(a), the active task requests a rendezvous
Figure 6.l(a)
active task
active task continuing
executing waiting
before the passive task is ready to receive the request and therefore
has to wait, but in figure 6.1(b), the passive task has to wait pending
Figure 6.1(b)
active task
active task executing continuing
passive task
MessageSystem:
declare
MessType = ...
task Producer;
task Consumer;
task BoundedBuffer is
entry Ge£ (mess: out MessType);
entry Pu£ (mess: in MessType);
end BoundedBuffer;
separate paths, the passive task executing its final statements. The
general layout of a passive task is shown in program 6.12.
6.6 Pascal-m
ADA gives us our first example of a language in which
constructs other than shared memory and shared memory access
controls are provided for communication between concurrent processes
and tasks. In ADA, however, it is possible to access variables within
parallel tasks which have been declared in an enclosing scope. In this
sense, ADA is a language which supports the shared memory model,
but it offers no facilities for protecting shared variables against
simultaneous access, and thus we must conclude that ADA'S preferred
170 Languages for Concurrency
send a to mbox
and:
send a to mbox;
Program 6.14
This latter construct does not create a mailbox, but merely creates a
variable capable of storing values of type "mailbox of T\ Thus the
statement
mbx: = mbox;
send a to mbx;
type T=...;
MOT = mailbox of T;
mailbox putbox,
getbox: MOT;
Program 6,15
176 Languages for Concurrency
select
if Guardi then MessageOfferi :
Statement i;
if Guard2 then MessageOffer2 :
Statement^
repeat
receive slots [p] from putbox;
p := (p + 1) mod BufSize;
send slots [c] to getbox;
c : = ( c + l ) mod BufSize
until false
end; { process instance BufferManager}
Program 6.16
Languages for Concurrency 111
processes which need to have access to them. If, on the other hand, we
had introduced our processes using process (as opposed to process
instance), we would then need to have provided separate instance
statements actually to create the processes and start their execution.
Using this method for declaring and starting processes allows the
possibility of creating multiple identical processes, and in this
particular example, multiple producers, consumers and buffer
managers could be created.
When processes (and modules if required) are introduced in
this way, it is permissible to specify a formal parameter list at the
declaration stage, and to supply actual parameters at instantiation.
Parameters may either be constants, or more commonly, mailboxes
which the particular instance of the process will use for
communication. Thus our message system might appear as in
programs 6.18 and 6.19.
If we now wished our system to be extended to contain another
producer/consumer pair, communicating through another bounded
buffer possibly of a different size, we should merely have to declare two
more mailboxes:
type r = . . . ;
MOT = mailbox of T;
mailbox putbox,
getbox: MOT;
than processes. One way in which clients may be able to receive replies
from a server would be through another known mailbox. That is, the
server must broadcast not only the identity of the mailbox (or
mailboxes) through which service requests may be submitted, but also
the identity of the mailbox(es) by which the client must receive the
reply.
This situation is far from satisfactory, since it is perfectly
possible for another process, either accidentally or maliciously, to
attempt to receive a message on the (well-known) reply mailbox, thus
preventing the true client from obtaining his response. This in itself is
highly undesirable, but if we further postulate that the service is
perhaps providing sensitive information, as, for instance, an
authentication service returning confidential authentication data, the
ability of other processes to "tap" replies could seriously compromise
the integrity of the whole system.
So we need to look for an alternative method for servers to
reply to their clients, and we may make use of Pascal-m's ability to
pass mailbox values in messages to provide such a mechanism. The
server is still required to publish the identity of the mailbox(es) by
which service requests are sent, but now we assume that a mailbox
value is sent with the request, and this is interpreted as the route by
which the reply is to be returned to the client. To demonstrate this, we
give a small example of a server which offers an arithmetic service. We
assume that this process takes two integers and, depending on the
mailbox through which the request arrives, returns the sum,
difference or product of the two. The result will of course be an integer.
Program 6.20 shows the module using a well-known mailbox to return
the result.
As indicated, the fact that TypicalClient calls receive
immediately following the send cannot guarantee that the results are
returned correctly, since any process within the system can have
access to the results mailbox, and may have already attempted a
receive naming this mailbox. This will succeed ahead of the receive
call made by TypicalClient.
A much safer method is illustrated in program 6.21(a) and (b).
In this case we wish to make our reply mailbox private to the
TypicalClient process. Unfortunately, mailbox definitions are not
permitted in a process declaration, so it is necessary for the typical
182 Languages for Concurrency
mailbox
add, subtract, mult: operbox;
mailbox
reply : mailbox of integer;
mailbox
nsrequestbox: NSinbox;
mailbox
nsanswerbox : mailbox of NSreply;
fsanswerbox: mailbox of FSreply;
6.7 OCCAM
The OCCAM language, invented by Inmos Ltd., takes a
somewhat different approach to concurrency than any of those
considered up till now, although some similarities with the previous
constructs may be identifiable. The language was developed as the
natural way in which to express the behaviour of a specific device
known as the transputer, but the language has much more
applicability than just to this device. However, in considering
programs written in OCCAM, it is useful to regard the constructs of the
language as representing hardware functions, and the communication
between language constructs as modelling the flow of information
between hardware components.
The primitives of the language consist of processes of three
types: input, output and assignment. Input/output is performed over
channels, and it is necessary for an input statement or process and an
output process specifying the same channel to be simultaneously
active for the input/output to take place. The assignment process is
used for assigning the value of an expression to a variable.
OCCAM processes are not unlike processes as we have already
discussed them, but in general they will be of much finer granularity.
In fact, the three types of process are represented in OCCAM by the
notation:
PAR
-- Comments are introduced by two dashes.
-- Producer code
WHILE true
VAR m:
SEQ
— produce message m
inchlm
~ Consumer code
WHILE true
VARm:
SEQ
outch?m
— consume message m
— Buffer code
WHILE true
-- See later
Program 6.23
consumer, a local variable m has been declared using the VAR
keyword. No type has been given to the variable, and for the time
being we shall assume that there are no types in OCCAM. Finally, we
have assumed in writing the producer and consumer code that the
buffer can accept messages on the channel inch, and will deliver
messages on the channel outch.
We now need to consider the implementation for the buffer
itself. The variables which we require are essentially the same as
those required in the previous versions, and they have the same
meanings. Program 6.24 shows an initial attempt.
We will first explain the action of this program and then
examine it in detail to determine whether or not it satisfactorily solves
the problem. The first step is to declare three local variables n, p and c,
and a local array b. These have precisely the same meanings as they
have done in the previous examples. Notice, however, that when we
190 Languages for Concurrency
ALT
(n < nmax) & inch?b[p] = >
PAR
n:= n + 1
SEQ
P —P + l
p \— p\ nmax
{n >0)&outch?ANY = >
PAR
n := n-1
SEQ
outchlb[c]
c:= c + 1
c : = c\ nmax
Program 6.25
a request has been made, and this could be detected by the presence of
an input (any input) on the output channel. The relevant part of the
revised solution is presented as program 6.25. The purpose of the
construct outchlANY is to allow for the situation where the
Languages for Concurrency 193
WHILE true
VARr
SEQ
inch?x
outchlx
Program 6.26
simple (one-place) buffer element. The operation of such a one-place
element is illustrated in program 6.26, and is represented
diagrammatically in figure 6.2
Figure 6.2
inch outch
PAR i = [0 FOR n]
WHILE true
VARx:
SEQ
ch[i]?x
ch[i +l]lx
Program 6.27
Notice that we have here been able to use the "control
variable" of the iteration to vary the channel's use for input and output
in each of the components of the expansion. The only modification of
the producer and consumer processes which arises from this change is
that the producer now sends items to the channel ch[0] instead of inch,
and the consumer receives them from ch[n] rather than outch. Using
this arrangement, we obtain something similar to the hardware FIFO
component which acts as a first-in-first-out buffer, exactly as we wish
the bounded buffer to behave. We also notice that our objection to the
first version of the OCCAM bounded buffer has also been removed.
Since each element of the buffer chain can only contain a single item,
clearly we can offer the contents of that buffer element as soon as it has
been delivered from the previous element. Thus the element at the
front of the buffer can simply offer the item it contains to the
consumer, and will only accept an item from the next element when
the consumer has removed the offered item. Each item as it is offered
to the buffer by the producer, will propagate along the chain until it
reaches the first free buffer element, at which point it will stop
pending the removal of an item by the consumer. When this happens,
all the remaining items in the chain will move along one place,
creating one more empty element. If this causes the element at the
back of the chain to be emptied, then the producer is free to submit
another item to the buffer. In a conventional (sequential)
programming language, all the additional copying of elements which
such a method implies would represent a considerable overhead in the
operation of the buffer, but in OCCAM, this additional effort is carried
out without delaying either the producer or the consumer by elements
which would otherwise be idle.
Languages for Concurrency 195
6.8 Exercises
6.4 Figure 6.9 ((a) and (b)) showed a solution in Path Pascal to the
bounded buffer problem for multiple producers and consumers.
The problem of interference between simultaneous puts or gets
was avoided by using the path
path 10: (1: (put); 1: (get)) end
i.e. by allowing only one put and one get to be active at any
time. By inspection of the code, however, it can be seen that
interference will only occur if simultaneous accesses are
attempted to the same buffer slot. Show how a Path Pascal
object (and path expression) may be constructed which controls
the incrementation of the get and put buffer pointers (the
variables p and c in program 6.9(b)) so that multiple
simultaneous calls to put and get may execute safely, and show
how this object would be used in the bounded buffer object to
achieve this relaxation in the concurrency constraints. What
196 Languages for Concurrency
6.9 Following on from the exercise 6.8, how would you provide a
name server in Pascal-m?
procedure ProcessCode;
begin
{initialisation code }
repeat
Yield
until false
end;
Program 7.1
We are now ready to make some preliminary definitions, and
to describe some important primitives which our Pascal machine will
need to provide. Firstly, we require the following Pascal definitions:
Implementation of a Concurrency Kernel 201
end;
ProcessTable = array ProcessID of
ProcessTableEntry;
WorkArea = record
addr: Memoryptr;
size: integer
end;
program Kernel;
{ definitions as before}
var p: Processld;
proctable: ProcessTable;
wsly ws29 ws3: WorkArea;
initst: State;
begin
{initialisation code }
p := NewProcess (ProcCodel, wsly initst);
proctable [p].procstate : = initst;
p : = NewProcess (ProcCode2y ws2y initst);
proctable [p].procstate : = initst;
p := NewProcess (ProcCode3, ws3y initst);
proctable [p].procstate : = initst;
p:= 1; { select first process to run (arbitrarily) }
repeat
{ kernel main loop}
until false
end.
Program 7.2
Thus, the action of the procedure Continue is to hide the state
of the kernel in some secret place known only to the kernel, and to pass
control to the process whose state is passed as the parameter of
Continue. To the kernel, this call of Continue is a procedure call, and
the kernel would expect to regain control as though a normal return
from Continue had occurred.
The actual return to the kernel is effected by the running
process making a call to Yield, which in turn calls GetState. The result
204 Implementation of a Concurrency Kernel
procedure Yield;
var st: State;
begin
GetState (st);
proctable [p].procstate : = st
end;
begin
repeat
{kernel main loop }
Continue (proctable [p].procstate);
p : = NextProcess (p)
until false
end.
Program 7.3
but it should be noted that Continue is called directly by the kernel,
whereas GetState is called (indirectly) by the process. However,
GetState must still be regarded as a kernel procedure, and is only
called by the processes as a result of their calling a kernel supplied
procedure, in this case, Yield.
The function NextProcess in the kernel main loop is simply an
encapsulation of the scheduling decision-making code - i.e. the code
Implementation of a Concurrency Kernel 205
used to decide which process should be the next to run, and perhaps the
simplest possible version of such a procedure would be represented by:
and during the initialisation of the kernel, we should have to make the
procedure call:
begin
if not kernelstate then
begin
GetState (st);
proctable[p].procstate := st
end
end:
Implementation of a Concurrency Kernel 209
would need to replace the body of the procedure Yield, and the same
alteration will be required for procedure ClockHandler. The question
still remains, however, as to where is the proper place to set and reset
this new variable. Given that interrupts can occur at arbitrary times
during the execution of unprotected code (i.e. most of the code we have
described), it is possible to imagine a situation, wherever the variable
kernelstate is updated, which will lead to incorrect behaviour. For
example, suppose we decided that the statement:
kernelstate: = true;
was required before every call of GetState, and suppose also that a
timer interrupt occurred during the call of Yield, after the test of
kernelstate had taken place, but before GetState was actually called.
We should be no better off than if we had not made the test at all!
If we examine all the possible places where this statement
might be inserted, we come to the conclusion that none of them is
correct. The only possible solution is to make the setting of kernelstate
part of the GetState operation, and to make the whole operation
indivisible. (In fact, as a precaution against programming errors while
the kernel was under development, the original version of GetState
was arranged to be a null operation when called with kernelstate set to
true. This had the added advantage that it could be a private variable
to the underlying Pascal support system, set by GetState and reset by
Continue, and the ability of the Pascal kernel to access the variable
became unnecessary.)
queue as they leave the processor, and to select the process at the head
of the queue to be the next running process. Unless some other action
is taken, the process which has been in the ready state for the longest
period of time is the next to run, as it would be in the simple version of
the kernel in the previous section. We now have the flexibility,
however, to alter the order of the processes in the queue should that be
thought to be desirable.
In order to represent the action of a queue, following Brinch
Hansen, we assume the following extension to Pascal. For any base
type T, the construct:
queue ofT
and:
The procedure Add simply adds item to the tail of the queue q, and
Remove takes the item from the head of q and returns it as the value of
the function. Both procedures have the side effect of altering the
queue.
The present simple version of the kernel may now be re-
written making use of the queue construct. Firstly, we require the
declaration:
begin
GetState (st);
proctable[p].procstate : = st;
Add (p, readyq)
end;
With this extension, we may still use the simple form of the procedure
NextProcess, but modifying it to become:
Actually, with this modification only, the value of runstate will never
be running for any process. In fact, this does not affect the performance
of the kernel, and the running process can always be identified by the
variable p, but for reasons of absolute completeness, we may wish to
include in the main loop of the kernel a statement which assigns the
value running to runstate in pfs process table entry prior to the call
Continue (p). If the running process is suspended as a result of a timer
interrupt, or by calling Yield, the value of runstate must also be
changed from running to ready. This change then must be made to the
bodies of both ClockHandler and Yield.
Let us now consider what action must be taken when a call is
made to wait on a semaphore, and also the action of signalling. Not
only does the version of the kernel which incorporates the semaphore
construct require queues for selecting the next process to run, but
when a process signals on a semaphore, it may have to select which of
possibly many processes waiting on the semaphore is to become ready.
(Recall that each signal only satisfies one of the waits which may have
214 Implementation of a Concurrency Kernel
proctable[p].runstate := ready;
and:
proctable[cand].runstate:— ready;
216 Implementation of a Concurrency Kernel
and;
7.6 Monitors
Semaphores, as we saw in chapter 3, are quite adequate as a
mechanism for providing inter-process communication, since they can
be used to exchange timing information, they can provide mutual
exclusion, and they can be the basis of a message passing system.
However, some more structured synchronisation mechanisms were
introduced in chapters 4 and 5, and it is instructive to examine how
semaphores might be used to handle the implementation of such
structures.
We shall begin by considering the monitor. The first constraint
imposed by the monitor is that only one process shall be inside a
monitor procedure at any one time. This part is easy to implement. All
that is required is a semaphore to provide the mutual exclusion, called
a monitor lock, A wait on the monitor lock is called at the entry to each
of the monitor procedures, and a signal is called on exit. The signal and
wait operations on a condition variable are almost as simple, except
that we must be careful to observe the difference between a condition
wait and a semaphore wait. We are not permitted to store signals on
conditions in the same way that signals on semaphores can be
accumulated. A signal on a condition variable for which no process is
waiting is equivalent to a "no-op", and a wait on a condition variable
will always wait, irrespective of the number of signals previously
performed on that condition. Thus to implement a condition variable
in terms of a semaphore, and in particular the signal and wait
operations on it, we must be permitted to examine the value of the
Implementation of a Concurrency Kernel 217
semaphore count field before either the wait or signal is carried out.
Thus, suppose that a monitor contained the declaration:
var c: condition;
and:
respectively.
Actually, the tests being performed here are identical to those
being made inside the semaphore wait and signal procedures, and thus
it would seem to be unnecessary to invoke the full power of signal and
wait. For this reason, and for other reasons which will become clear
later, it is appropriate for the queuing and de-queuing to be done
explicitly in the implementation of c.wait and c. signal
Unfortunately, this is not the whole solution, because a process
executing the c.wait statement must be inside the monitor, and if it
simply waited on the condition semaphore, it would still be holding the
monitor lock, thus preventing any other process from entering the
monitor. Neither is it simply a question of releasing the monitor lock
just before calling wait (csem), because when another process executes
the corresponding signal (csem), the process which is woken up by this
action would merely join the other processes competing to enter the
218 Implementation of a Concurrency Kernel
which returns the value true if there are no items in the queue q, and
false otherwise. Now the implementation of condition.wait is simply a
matter of the process attaching itself to the appropriate queue,
marking itself as blocked, and releasing the monitor lock.
Implementation of a Concurrency Kernel 219
path p end
path a; b end
implies that the operation b may not be executed until the operation a
has been completed. (Recall also that a and b could themselves be
paths.) There is no other constraint implied by this path, and thus as
many simultaneous activations of the pair a then b as are desired may
be in progress at any given time. In order to guarantee the
222 Implementation of a Concurrency Kernel
path a, b end
path N: a end
and the meaning of this construct was that if one activation of the
operation within the bracket was in progress, then another could start,
but if all activations ceased, then this element of the path was
regarded as having terminated. Constructing non-trivial examples of
the use of burst is not easy, and on many occasions the simple choice
("comma") appears to be indistinguishable from burst mode. The real
power of the construct is to make multiple activations of an operation
appear to be a single activation, as in the solution in section 4.5 of the
readers and writers problem. As a reminder, we recall that the path
used to describe the concurrency requirements in that example was:
and:
7.8.1 OCCAM
In simple cases, such as the message passing facilities of
OCCAM, it is possible to implement a channel in terms of a single
memory location controlled by a single semaphore. Then, the OCCAM
statements:
Implementation of a Concurrency Kernel 225
and:
7.8.2 ADA
In many ways, the interaction between tasks in ADA is very
similar to that of OCCAM. When an active task calls an entry point in a
passive task at a rendezvous, the call and the parameters may be
thought of as a message. There is no buffering of such calls, and
therefore a mechanism very similar to that described above for OCCAM
will provide the necessary synchronisation. In the case of ADA,
however, the calling (active) task is not permitted to continue until the
whole of the rendezvous is complete, and therefore the signal which
allows the active task to continue must be delayed until the completion
of the rendezvous code.
226 Implementation of a Concurrency Kernel
task ActiveTask;
PassiveTask.EntryPt (pars);
end ActiveTask;
Program 7.8
task PassiveTask is
entry EntryPt (pars: in out ParamType);
end PassiveTask;
or
when BooleanExpression — >
accept EntryPt (pars: in out ParamType) do
end EntryPt;
or
end select;
end loop;
end Passive Task;
Program 7.9
rendezvous with the passive task in program 7.9. As far as the message
itself is concerned, we shall assume that the identity of the entry point
being requested, and the values/addresses of the parameters involved
in the interaction are all passed across to the passive task in a way
Implementation of a Concurrency Kernel 227
7.9 Exercises
7.2 In the exercise 4.5 (chapter 4), which required the construction
of a monitor to handle the buffering of characters being printed
on a line printer, the routine GetChar would be called by the
printer device driving process. In practice, it would probably be
228 Implementation of a Concurrency Kernel
This bibliography does not aim to be in any way exhaustive, but just to
provide a few pointers into the vast wealth of literature on the topic of
concurrent programming. We begin by referring to other textbooks on
the subject. As has been pointed out already (in the Preface) the
subject of concurrent programming essentially grew out of a study of
operating systems, and many, if not most, textbooks on operating
systems include a section on concurrency. A few such text books are
suggested:
Brinch Hansen, P.,
Operating System Principles. Prentice Hall. (1973).
Deitel,H.M.,
Operating Systems. Second Edition. Addison Wesley. (1990).
Habermann, A.N.,
Introduction to Operating System Design. Science Research
Associates. (1976).
Silberschatz, A., Peterson, J.L., and Galvin, P.B.,
Operating System Concepts. Third Edition. Addison Wesley. (1991).
Tsichritzis, D.C., and Bernstein, A.J.,
Operating Systems. Academic Press. (1974).
Dijkstra,E.W.,
Solution of a problem in concurrent programming control. Comm
ACM8,p.569.(1965).
Co-operating sequential processes. In Programming Languages, F.
Genyus (ed.), Academic Press, pp.43-112. (1967).
Hierarchical ordering of sequential processes. Ada Informatica 1,
pp. 115-138.(1971).
Guarded commands, non-determinacy, and the formal derivation of
programs. Comm ACM 18, pp. 453-457. (1975).
Hoare, C.A.R.,
Towards a theory of parallel programming. In Operating Systems
Techniques, C.A.R.Hoare and R.H.Perrott (eds.), pp.61-71.
Academic Press. (1972).
Monitors, an operating system structuring concept. Comm ACM 17
pp. 549-557. (1974).
Horning, J.J., and Randell, B.,
Process structuring. ACM Computing Surveys 5, pp. 5-30. (1973).
Lamport, L.,
A new solution of Dijkstra's concurrent programming problem.
Comm ACM 17, pp. 453-455. (1974).
Peterson, G.L.,
Myths about the mutual exclusion problem. Information Processing
Letters 12, pp. 115-116. (1981).
The following books and articles are related to the specific languages
to which reference has been made in the text. In some cases, notably
ADA and OCCAM, we refer to early papers which give the initial
flavour of the language. In many cases, programming texts have also
been published which may be of interest to readers who wish actually
to write programs in these languages.
Abramsky, S., and Bornat, R.,
PASCAL-M: a language for distributed systems. QMC CS Lab
Report 245, Queen Mary College, London. (1980).
232 Bibliography
Barnes, J.G.P.,
An overview of ADA. Software - Practice and Experience 10, pp. 851-
887.(1980).
Brinch Hansen, P.,
The programming language Concurrent Pascal. IEEE Trans on
Software Engineering. SE-1 2, pp.199-206. (1975).
Campbell, R.H., Greenberg, I.B., and Miller, T.J.,
Path Pascal User Manual. Technical Report UIUCDCS-R-79-960,
Department of Computer Science, University of Illinois at Urbana-
Champaign. (1979).
Campbell, R.H., and Habermann, A.N.,
The specification of process synchronisation by path expressions.
Lecture Notes in Computer Science 16, Springer-Verlag, pp.89-102.
(1974).
Hoare, C.A.R.,
Communicating sequential processes. Comm ACM 21, pp. 666-677.
(1978).
Communicating Sequential Processes. Prentice Hall Intl. (1985).
Inmos Ltd,
OCCAM Programming Manual Prentice Hall Intl. (1984).
Lampson, B.W., and Redell, D.D.,
Experience with processes and monitors in Mesa. Comm ACM 23,
pp. 105-117.(1980).
May, D.,
OCCAM , ACM SigPlan Notices 18, No. 4, pp. 69-79. (1983).
Mitchell, J.G., Maybury, W., and Sweet, R.E.,
Mesa Language Manual, version 5.0. Palo Alto Research Center
Report CSL-79-3, Xerox Corp. (1979).
Welsh, J., and Bustard, D.W.,
Pascal Plus - another language for modular multiprogramming.
Software - Practice and Experience 9, pp. 947-957. (1979).
Bibliography 233
PlantlnterruptHandler 206
R
priority 97, 209
Randell 34
procedure-based 188
readers and writers 93
process 15,140,148,149,152,
161,187 ready 210,212,215
client 115,163 ready queue 209,210
concurrent 15 real-time 161
creation 27,28,31 receive 114,171,187
formal model 34 selective 117
identity 118 receiver, receiving process, see
consumer
informal definition 7
recv system call 128
representation of 15
region 30,36-39,81,83
sequential 15
rendezvous 162,225,227
server 115,117,119,120,
163 resource, see also shared
resource 42
state 19,37
UNIX 31,123,125
process table 201,202 scheduling 139, 204
process table entry 205, 213 disk head 96,139
producer 62,115,131, 141,151, non-pre-emptive 205, 212
161,165,174,188 policy 205, 212
producer/consumer problem 63, pre-emptive 205
131,141,161
priority 220
program counter 37 — 38
semaphore 57,212,214
programming, structured 4, 78,
80,84,89-90,162 as timing signal 59
programming language 16,25, binary 57, 78
27, 84,122,139,140,148,194, for exchanging information
227 62
programming error 78, 80,143, monitor lock 217
209 mutual exclusion 56, 80, 222
pseudo-concurrency 9 non-binary 67
Q properties of 69
Queen Mary College, London send 114,144,170,171
26 sender, sending process, see
Queen's University, Belfast 26 producer
queue 143,210 send system call 128
condition 94, 218 sequential process 7,15,174
monitor lock 209 sequential program 4,157
semaphore 214 server, see process, server
238 Index
TAS,seeTest-and-Set
task 45, 60, 226
Test-and-Set 55, 56
threads of control 16
Toronto, University of 147
transputer 187
U
UNIX 31,123
V-operation 57
Van Horn 16