Ada is one of the few mainstream languages that supports concurrency from within the language. This has the benefit of stable semantics, and therefore portability between computers. The task and rendezvous mechanism of Ada83 has been enhanced through the introduction of protected objects and greater considertion for real time systems.
The death of a task does not necessarily affect the operation of other tasks, except if they attempt to communicate with it. There is no specialised task, even the main procedure of the Ada program can end, with other tasks continuing.
As one mechanism for tasks to safely communicate and synchronise with each other, the rendezvous is conceptually simple. Tasks publish entry points at which they are prepared to wait for other tasks. These entry points have a similar semantics to procedures; they can have parameters of in, inout and out mode, with default parameter values. A task wishing to rendezvous with a second task simply makes a procedure-like rendezvous call with an entry; Either is held up until they can both communicate. The rendezvous is non symmetrical - one task is viewed as a server and cannot initiate a rendezvous.
procedure demo is
task single_entry is entry handshake; end task; task body single_entry is begin delay 50.0; accept handshake; delay 1.0; end; begin for i in 1..random(100) loop delay(1.0); end loop; handshake; end;
When this program is run the task single_entry is started. If succeful the parent task is also activated. If 'random' returns a value less than 50, then after the initial delay the main task waits for the second task, and vice versa if it returns greater than 50. Once the two tasks have rendezvoused, they depart on their separate ways. The main task finishes, the second task continues until it too finishes. The program then terminates.
The same program can be modified to include parameters in the entry call. In this example, the two tasks swap duration times for the delay statements. However they may still not finish together as the quicker task to the rendezvous will still have to wait for the second task.
procedure demo is
x :duration := duration(random(100));
y :duration;
task single_entry is
entry handshake(me_wait: in duration; you_wait: out duration); end task; task body single_entry is a :duration := duration(random(100)); b :duration; begin delay a; accept handshake(me_wait:in duration; you_wait: out duration) do
b := me_wait;
you_wait := a;
end handshake; delay b; end; begin delay(x); handshake(x,y);
delay(y); end;
A task can have several rendezvous'. If it waits for a rendezvous and there is no possible task that can rendezvous with, then it will abort with a tasking_error exception.
Rendezvous' can be treated in a manner very similar to procedure calls. They can be in loops, if statements, subprograms etc. Typically however they are structured in a high level loop; this allows a client architecture in which after servicing a rendezvous, and performing some processing, it goes back and waits for another rendezvous.
procedure demo is
type service is (increment, decrement, quit);
task single_entry is entry accept_command(which : service); end task; task body single_entry is
command :service;
i :integer; begin loop
accept accept_command(which :service) do
command := which;
end accept_command;
case command is
when increment => i := i + 1;
when decrement => i := i - 1;
when quit => exit; end case; end loop; end; begin -- stuff, depending perhaps upon user input. end;
This situation can be impoved having multiple entry points, and making use of the select statement in conjunction with the accept statement.
procedure demo is
task multiple_entry is
entry increment;
entry decrement;
end multiple_entry;
task body multiple_entry is
i : integer;
begin
select
accept increment;
i := i + 1;
or
accept decrement do
i := i - 1;
end decrement
or
terminate;
end select;
end;
begin
-- rendezvous'
end;
The select statement allows the task to wait on several different entry points. This example shows the increment entry without any associated processing. The decrement entry forces the calling task to wait while the decrement takes place. The terminate alternative causes task multiple_entry to finish if it is uncallable from any other task.
The select statement can have a selective wait, unconditional alternative, a terminate or none of these. The previous example showed the terminate. The unconditional alternative is shown below.
procedure demo is
task multiple_entry is entry increment; entry decrement; end multiple_entry; task body multiple_entry is
i : integer; begin
select
accept increment; i := i + 1; or accept decrement do i := i - 1; end decrement else perform_some_processing; end select; end; begin -- rendezvous' end;
In this example, the task stops only briefly to inspect if another task is waiting to rendezvous. If not it goes off and does some processing.
The selective wait allows the task to wait for a given time, after which it gives up and continues on with other actions.
procedure demo is
task multiple_entry is
entry increment;
entry decrement;
end multiple_entry;
task body multiple_entry is
i : integer;
begin
select
accept increment; i := i + 1; or accept decrement do i := i - 1; end decrement or delay 3.0; -- not a normal delay statement! perform_some_processing; end select; end; begin -- rendezvous' end;
Here the task will wait for at least 3.0 seconds for another task. If none comes along it will call perform_some_processing.
Similarly the client task can either wait unconditionally for a rendezvous, it can not wait at all (impatient), or it can specify a time out after which it will give up on a rendezvous.
All of the above tasks are of an anonymous type. Ada allows you to create a task type (via a special and non consistent syntax from all other type declarations) and to declare objects of the type. They can be declared dynamically (through an allocater) as well as local variables. They can be composed into other objects - you could have an array of tasks, or a record, a component of which, is a task.
The rendezvous mechanism is a synchronisation as well as a communication mechanism. Tasks that should normally run asynchronously and which want to pass data between them are required to stop and "have a chat", instead of simply leaving the data for the other task. The solution to this problem is usually to creat an intermediary task that manages the data flow, and, the designer hopes, is always available to respond to a rendezvous from either task. It sits between the two tasks, acting as an emissary for both.
A simpler solution can be provided in Ada95 which has a concept of a protected object or type. Protected objects provide for task safe access to data via entries and guards, without the overhead of a separate task being created. The subprograms and entries inside a protected object are executed in the context of the calling task. As with tasks types you can create arrays of protected type, and compose other objects from them.
All the examples in this section come from Introducing Ada9x, John Barnes.
An example of a protected entry follows
protected Variable is
function Read return item;
procedure Write(New_Value:item);
private
Data :item;
end Variable;
protected body Variable is
function Read return item is
begin
return Item;
end;
procedure Write(New_value :item) is
begin
data := New_Value;
end;
end Variable;
Functions are only allowed read access to the data, procedures can manipulate them in any way they wish. Procedure calls are exclusive, that is only one task can access a procedure at any one time. Multiple reads can occur concurrently.
Another example is a semaphore, to provide exclusive access to resources.
protected type Counting_Semaphore (Start_count :Integer := 1) is
entry Secure;
procedure Release;
function Count return integer;
private
Current_count :integer := start_count;
end;
protected body counting_semaphore is entry Secure when Current_count > 0 is begin Current_Count := Current_count + 1; end; procedure Release is begin Current_Count := Current_count + 1; end; function Count return integer is begin return current_count; end; end Counting_semaphore;
When called the barrier on the entry is queued. If it is false, the task is queued, pending it becoming true.