Ada allows concurrency to be specified by a language construct known as a task.
A non-concurrent Ada program contains a single main procedure that is started when the program is started; and when the procedure terminates the program terminates.
A concurrent Ada program can include any number of tasks - declared by
task <name> isinstead of
procedure <name> isWhen the program is started, each task is started, and all tasks run in parallel with one another and with the main program. Of course, some tasks may terminate while others are still running. When all tasks terminate (including the main program), the overall program terminates.
The Ada syntax for task declaration is actually quite complex - one can create data objects of type task, and can thus have arrays of tasks etc. We will not go into this now.
Communication between concurrent tasks is a more difficult problem.
Ada allows both shared variables and message passing for inter-task communication. We will consider only the latter, because it is the only facility that allows synchronization of such accesses. (However, if needed the message passing mechanism could be used to implement something like a semaphore, which could then be used to protect shared data!)
The Ada message-passing mechanism is called a rendezvous.
An Ada task has a two part declaration: a task specification and a task body.
If a task never receives messages from other tasks, then its task specification will be empty (though still required by the syntax.)
In general, a task specification can include one or more declarations of entry points. These represent kinds of messages the task is willing to receive.
For our remaining examples, we will use a variant of the producer and consumer problem having three tasks: a producer task, a consumer task, and a buffer task.
The producer task produces characters and stores them in the buffer.
The consumer task removes characters from the buffer and consumes them.
The specification for the buffer task would might like this - with three entry points:
task BUFFER is entry STORE(C: in CHARACTER); -- Put a character into -- the buffer entry RETRIEVE(C: out CHARACTER); -- Take one out entry PRODUCER_DONE; -- PRODUCER has shut down end BUFFER;
Note: two entry points are used during routine operation; the third is used when the system is about to shut down.
Notice that the declaration of an entry point looks very much like the declaration of a procedure. In fact, from the standpoint of the task invoking an entry, the syntax is the same as for a procedure call.
A rendezvous occurs when one task invokes an entry point of another task.
Example: the following line might occur in the producer task:
BUFFER.STORE(CARD(I));
However, unlike an ordinary procedure call, both tasks have to actively participate in a rendezvous. An entry call can only be completed if the recipient task has executed an accept statement for that entry. For example, the following might appear in the body of the buffer task (where the buffer is implemented as a queue using a circular array.):
accept STORE(C: in CHARACTER) do BUFFER_DATA(BUFFER_REAR) := C; BUFFER_REAR := (BUFFER_REAR + 1) rem BUFFER_SIZE; BUFFER_EMPTY := FALSE; BUFFER_FULL := BUFFER_REAR = BUFFER_FRONT; end STORE;
(Note that an accept statement can occur in the middle of the executable code of the accepting task. When execution gets to that point, then the task is ready to accept the rendezvous.)
Since a rendezvous can occur only with the cooperation of both tasks, it is possible that one task will have to wait for the other.
If the producer task attempts to execute the STORE rendezvous when the buffer is not ready to accept it, the producer task will be made to wait until the buffer is ready.
If the buffer task attempts to accept the STORE rendezvous when no one is trying to do a store, the buffer task will be made to wait until the producer does so.
Of course, this raises a question in the case of a task like the buffer, which has several entry points. What if it tries to accept a STORE entry when the producer has run out of cards and is trying to invoke the PRODUCER_DONE entry instead?
The buffer will wait forever for an entry call that will never occur.
The producer will wait forever for the buffer to be ready to accept PRODUCER_DONE, while it is hung elsewhere.
To deal with this kind of situation (which is very common), Ada provides a select statement whereby a task can announce its readiness to accept any one of several possible entries:
select when not BUFFER_FULL => accept STORE(C: in CHARACTER) do BUFFER_DATA(BUFFER_REAR) := C; BUFFER_REAR := (BUFFER_REAR + 1) rem BUFFER_SIZE; BUFFER_EMPTY := FALSE; BUFFER_FULL := BUFFER_REAR = BUFFER_FRONT; end STORE; or when not BUFFER_EMPTY => accept RETRIEVE(C: out CHARACTER) do C := BUFFER_DATA(BUFFER_FRONT); BUFFER_FRONT := (BUFFER_FRONT + 1) rem BUFFER_SIZE; BUFFER_EMPTY := BUFFER_FRONT = BUFFER_REAR; BUFFER_FULL := FALSE; end RETRIEVE; or accept PRODUCER_DONE do PRODUCER_IS_DONE := TRUE; end PRODUCER_DONE; end select;
This select announces the buffer's readiness to accept any one of the three possible entries.
If the select is executed when no entry call is pending, then the buffer task will wait for one to occur. If it is executed while one entry call is pending, then that rendezvous will occur at once. If it is executed when two or more entry calls are pending, then one rendezvous will occur - but which one it will be is not specified by the language - it could be any one of them.
Note further that, in this case, two of the entries are qualified by when clauses:
the STORE entry cannot be accepted when the buffer is full - only RETRIEVE or PRODUCER_DONE is allowed.
the RETRIEVE entry cannot be accepted when the buffer is empty - only STORE or PRODUCER_DONE is allowed.
PRODUCER_DONE may be accepted at any time.
Not shown here is the possibility that a select statement may also include an else clause. In this case, if the select is executed and no entries are pending, then the else part is done immediately (the task does not wait.)
In this respect, the Ada rendezvous is assymetric:
A task that attempts an entry call which the destination task is not ready to accept will always be put into a wait state.
A task that attempts to accept a rendezvous has considerable freedom, and need not be forced to wait for that specific rendezvous or even for any rendezvous at all.
With this background, we are ready to look at a complete implementation of the producer and consumer problem (with a buffer) in Ada.
This code actually contains a subtle but serious error. It can deadlock when it executes (and, in fact, it usually does with our Ada system.)
Can you spot the source of the problem?
How would you fix it?
We can also look at the sorting race we demonstrated earlier (which is totally correct.) Source Code.
$Id: concurrency9.html,v 1.3 1998/03/03 23:42:05 senning Exp $
These notes were written by Prof. R. Bjork of Gordon College. In February 1998 they were edited and converted into HTML by J. Senning of Gordon College.