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 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.
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.
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.