|
by Jim Rogers
This is part five (of five) of this article. Click here
to return to part four, or click here to return to the article's
table of contents.
Ada provides an extensive set of capabilities for
creating programs with concurrent code modules. Java achieves
many of the same results using the Thread class. An Ada
concurrent code module is called a task.
When you create a task you can choose to make a
one of a kind task, or a task type which can be used to create
many identical tasks. Tasks andtask types are defined in two
parts. The first part defines the public interface to the task,
specifying any entry calls. The second part contains the
implementation of the task code. Ada tasks can communicate with
each other directly using the rendezvous mechanism. A rendezvous
creates a synchronization or meeting point between the task
calliing another task's entry and the called task. The first task
to the rendezvous will suspend until another task gets to
the same rendezvous.
Task Definition
task Simple_Task is
entry Start(Num : in Integer);
entry Report(Num : out Integer);
end Simple_Task;
task body Simple_Task is
Local_Num : Integer;
begin
accept Start(Num : in Integer) do
Local_Num := Num;
end Start;
Local_Num := Local_Num * 2;
accept Report(Num : out Integer) do
> Num := Local_Num;
end Report;
end Simple_Task;
The task Simple_Task is declared to have
two entries: Start and Report. Another task can
communicate with Simple_Task by calling Start and
passing in an Integer, or by calling Report and
passing out an Integer. Simple_Task starts
executing as soon as the program starts, but it does not get very
far. It encounters the accept Start statement as its first
executable statement. Simple_Task suspends at that accept
statement until some other task calls its Start entry.
During the Start entry Simple_Task assigns
the value in the formal parameter Num to the local
variable Local_Num . This assignment is necessary because
the Num parameter is only in scope between the do
and done statements. Upon completion of this rendezvous Simple_Task
continues executing until it encounters another accept
statement. Simple_Task again suspends if no
other task has yet called its Report entry. When another
task does call the Report entry the value of Local_Num
is copied to the parameter Num. Upon
completion of the Rendezvous the calling task has the
value from Local_Num and Simple_Task completes
because there are no more statements to execute.
The example of Simple_Task shown above has
one major limitation. It is a one time definition of a single
task. If you want to make many instances of this task you need to
create a task type. The only difference in syntax is the addition
of the word type in the task interface code.
task type Simple_Task is
entry Start(Num : in Integer);
entry Report(Num : out Integer);
end Simple_Task;
task body Simple_Task is
Local_Num : Integer;
begin
accept Start(Num : in Integer) do
Local_Num := Num;
end Start;
Local_Num := Local_Num * 2;
accept Report(Num : out Integer) do
Num := Local_Num;
end Report;
end Simple_Task;
With the creation of a task type we have the
opportunity of making as many instances as we need.
type Task_Pool is array(Positive range 1..10) of Simple_Task;
My_Pool : Task_Pool;
Declaration of the array type does not create any
tasks. Declaration of the array instance creates 10 instances of
Simple_Task. All this can be put together in a small program.
with Ada.Text_IO; use Ada.Text_Io;
procedure Test_Simple is
task type Simple_Task is
entry Start(Num : in Integer);
entry Report(Num : out Integer);
end Simple_Task;
task body Simple_Task is
Local_Num : Integer;
begin
accept Start(Num : in Integer) do
Local_Num := Num;
end Start;
Local_Num := Local_Num * 2;
accept Report(Num : out Integer) do
Num := Local_Num;
end Report;
end Simple_Task;
type Task_Pool is array(Positive range 1..10) of Simple_Task;
My_Pool : Task_Pool;
The_Value : Integer;
begin
for num in My_Pool'Range loop
My_Pool(num).Start(num);
end loop;
for num in My_Pool'Range loop
My_Pool(num).Report(The_Value);
Put_Line("Task" & Integer'Image(num) & " reports"
& Integer'Image(The_Value));
end loop;
end Test_Simple;
The Ada rendezvous mechanisim is useful but there
are many designs that require a more asynchronous behavior
between tasks. Ada provides an elegant and powerful approach to
creating objects that can be shared between tasks. Those object
are called Protected Objects. Just as with tasks,
you can make a single version or a Protected Type, which
allows you to create many instances of the same kind of shared
memory object. Protected objects are protected from inappropriate
mutual access by tasks.
A task may need to respond to one of many entry
calls each time through its major control loop. A task may need
to check if an entry has been called, but proceed immediately if
it has not. Alternatively a task may need to wait for an entry
call, but no more than a specified amount of time. Ada provides
forms of selective accept calls for this purpose.
Selective Accept
loop
select
accept Stop;
exit;
else
Put_Line ("Not stopped yet");
end select;
delay 0.01;
end loop;
This example shows an infinite loop. Each time
through the loop the code selectively accepts the Stop
entry for this task. If the entry is accepted the exit
command is executed, terminating the loop. If no task has called
the Stop entry the code prints Not stopped yet then
delays (suspends) for 0.01 seconds.
loop
select
accept Stop;
exit;
or
delay 0.01;
end select;
Put_Line ("Not stopped yet");
end loop;
The syntax of this example is slightly different
from the previous example. The example still has an infinite loop
that checks if the Stop entry has been called each time
through the loop. In this example the task will simultaneously
start a delay timer. If the delay expires before the Stop
entry is called the string Not stopped yet is printed. If
the Stop entry is called before the timer expires the
timer is cancelled and the exit command is executed,
terminating the loop.
loop
select
accept Stop;
exit;
or
accept Put(Item : in Integer) do
Local_Item := Item;
end Put;
Local_Item := Local_Item * 2;
else
Put_Line("No entry calls this time");
end select;
delay 0.01;
end loop;
Each time through this loop the task checks to
see if either Stop or Put has been called. If Stop
has been called the exit command is executed, terminating the
loop. If Put has been called Local_Item is assigned the
value of Item, then that value is multiplied by 2. If neither
entry has been called the task prints No entry calls this time.
If the loop has not been terminated the task delays for 0.01
seconds and repeats the loop. A selective accept may have several
accept alternatives.
There are three kinds of operations on protected
objects.
- Procedures are used to unconditionally changes the
state of the protected object. Procecures must
have exclusive access to the protected object. The good
news about that requirement is that the compiler
generates all the code required to make exclusive access
happen.
- Entries are also used to change the state of the
protected object. The difference between entries
and procedures is that entries have a
boundary condition. They can only be executed when the
boundary condition is true. If a task calls a
protected entry when the boundary condition is false
the call will be placed in an entry queue, and only
executed when the boundary condition becomes true.
- Functions are used to report the state of a
protected object. Since functions do not change the state
of the protected object it is legal for several tasks to
simultaneously access a protected object through function
calls.
The following protected object implements a
counting semaphore. It allows up to 5 tasks to simultaneously
hold the semaphore.
protected type Counting_Semaphore is
entry Acuire;
procedure Release;
function Count return Natural;
private
Holding_Count : Natural := 0;
end Counting_Semaphore;
protected body Counting_Semaphore is
entry Acquire when Holding_Count < 5 is
begin
Holding_Count := Holding_Count + 1;
end Acquire;
procedure Release is
begin
if Holding_Count > 0 then
Holding_Count := Holding_Count - 1;
end if;
end Release;
function Count return Natural is
begin
return Holding_Count;
end Count;
end Counting_Semaphore;
This example demonstrates the use of all three
protected operations. Protected types allow you to define any
necessary shared memory design.
When a task calls an entry, that call may be
queued up due to a closed boundary condition. The calling task
may not be able to suspend indefinitely due to strict timing
requirements. If this is the case the calling task can use a
selective entry call. This can either be a timed entry call,
supplying a timeout, or a conditional entry call, providing an
immediate alternative.
select
Semaphore.Acquire;
Acquired := True;
or
delay 0.15;
Acquired := False;
end select;
This shows how a task could try to acquire a
counting semaphore as shown above, but wait no more than 0.15
seconds for success.
select
Semaphore.Acquire;
else
raise Resources_Blocked;
end select;
This example attempts to immediately acquire the
semaphore. If the semaphore is not immediately available the
exception Resources_Blocked is raised.
Ada allows generic programming, similar to C++
templates. You can define any compilation unit to be generic.
This allows you to define an algorithm independent of the data
type it must be used with. If you want to create a generic
package or procedure and allow the use of any non-limited type
then you must delcare the formal generic parameter to be private.
generic
type Element_Type is private;
procedure Swap(Left, Right : in out Element_Type) is
Temp : Element_Type := Left;
begin
Left := Right;
Right := Temp;
end Swap;
This procedure must be instantiated for a
specific type to be used.
with Swap;
procedure Swap_Test is
procedure Exchange is new Swap(Integer);
A : Integer := 6;
B : Integer := -19;
begin
Exchange(A, B);
end Swap_Test;
In this example procedure Exchange is
instantiated as a version of Swap taking Integer
values as parameters. The generic procedure is instantiated in
the declarative region of procedure Swap_Test . Exchange
is called in the body of the procedure. After the call to Exchange,
A will contain -19 and B will contain 6. Generic instantiation is
performed at compile time. This allows the compiler to perform
all parameter type checks on the call.
Generics are very useful when defining protected
objects containing a buffer.
generic
type Buffer_Type is private;
package Generic_Buffer is
protected type Buffer is
entry Get(Item : out Buffer_Type);
procedure Put(Item : in Buffer_Type);
private
Internal : Buffer_Type;
Is_New : Boolean := False;
end Buffer;
end Generic_Buffer;
package body Generic_Buffer is
protected body Buffer is
entry Get(Item : out Buffer_Type) when Is_New is
begin
Item := Internal;
Is_New := False;
end Get;
procedure Put(Item : in Buffer_Type) is
begin
Internal := Item;
Is_New := True;
end Put;
end Buffer;
end Generic_Buffer;
This generic buffer allows the writing task to
write to the buffer unconditionally. The reading task can only
read new data. It cannot read uninitialized data nor data it has
already read. This buffer pattern is used to allow the reader to
sample the output of the writer at any rate equal to or less than
the rate the writer writes to the buffer. In other words, the
reader will be no faster than the writer.
Ada is an effective language for the economical
creation of correct software. Along with fairly common features
such as modularity and the ability to program by extension, Ada
adds a sophisticated set of tools for concurrent programming and
a very rich type system that allows you to create your own
customized primitive types. The compiler can then use those
customizations to determine coding errors. The compiler will
also, as a default behavior, automatically produce safety and
correctness checks for run time error detection.
The cost of Ada compilers ranges from free to
expensive. The free compilers are very robust and complete, but
come with no support. The commercial compilers come with enhanced
development environments and very strong support. Most of the
support offered comes in the form of training in the Ada
language. Every Ada compiler currently on the market has passed
the compiler correctness test suite. This ensures that Ada
programs are highly portable across compilers as well as across
operating systems.
Ada has established itself as the world's premier
language for safety critical applications. It has also been shown
to be among the most economical languages to use for serious
programming efforts.
Click here to return to the start of this article,
or here to return to the table of contents.
|