Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

5.3 Synchronized interfaces

We now turn to the most important improvement to the core tasking features introduced by Ada 2005. This concerns the coupling of object oriented and real-time features through inheritance.
Recall from the chapter on the object oriented model (see Section 2.4) that we can declare an interface thus 
type Int is interface;
An interface is essentially an abstract tagged type that cannot have any components but can have abstract operations and null procedures. We can then derive other interfaces and tagged types by inheritance such as 
type Another_Int is interface and Int1 and Int2;
type T is new Int1 and Int2;
type TT is new T and Int3 and Int4;
Remember that a tagged type can be derived from at most one other normal tagged type but can also be derived from several interfaces. In the list, the first is called the parent (it can be a normal tagged type or an interface) and any others (which can only be interfaces) are called progenitors.
Ada 2005 also introduces further categories of interfaces, namely synchronized, protected, and task interfaces. A synchronized interface can be implemented by either a task or protected type; a protected interface can only be implemented by a protected type and a task interface can only be implemented by a task type.
A nonlimited interface can only be implemented by a nonlimited type. However, an explicitly marked limited interface can be implemented by any tagged type (limited or not) or by a protected or task type. Remember that task and protected types are inherently limited. Note that we use the term limited interface to refer collectively to interfaces marked limited, synchronized, task or protected and we use explicitly limited to refer to those actually marked as limited.
So we can write 
type LI is limited interface;    -- similarly a type LI2
type SI is synchronized interface;
type TI is task interface;
type PI is protected interface;
and we can of course provide operations which must be abstract or null. (Remember that synchronized is a new reserved word.)
We can compose these interfaces provided that no conflict arises. The following are all permitted:
type TI2 is task interface and LI and TI;
type LI3 is limited interface and LI and LI2;
type TI3 is task interface and LI and LI2;
type SI2 is synchronized interface and LI and SI;
The rule is simply that we can compose two or more interfaces provided that we do not mix task and protected interfaces and the resulting interface must be not earlier in the hierarchy: limited, synchronized, task/protected than any of the ancestor interfaces.
We can derive a real task type or protected type from one or more of the appropriate interfaces
task type TT is new TI with
   ...    -- and here we give entries as usual
end TT;
or
protected type PT is new LI and SI with
   ...
end PT;
Unlike tagged record types we cannot derive a task or protected type from another task or protected type as well. So the derivation hierarchy can only be one level deep once we declare an actual task or protected type.
The operations of these various interfaces are declared in the usual way and an interface composed of several interfaces has the operations of all of them with the same rules regarding duplication and overriding of an abstract operation by a null one and so on as for normal tagged types.
When we declare an actual task or protected type then we must implement all of the operations of the interfaces concerned. This can be done in two ways, either by declaring an entry or protected operation in the specification of the task or protected object or by declaring a distinct subprogram in the same list of declarations (but not both). Of course, if an operation is null then it can be inherited or overridden as usual.
Thus the interface
package Pkg is
   type TI is task interface;
   procedure P(X: in TI) is abstract;
   procedure Q(X: in TI; I: in Integer) is null;
end Pkg;
could be implemented by 
package PT1 is
   task type TT1 is new TI with
      entry P;     -- P and Q implemented by entries
      entry Q(I: in Integer);
   end TT1;
end PT1;
or by
package PT2 is
   task type TT2 is new TI with
      entry P;    -- P implemented by an entry
   end TT2;
   ...    -- Q implemented by a procedure
   procedure Q(X: in TT2; I: in Integer);
end PT2;
or even by 
package PT3 is
   task type TT3 is new TI with end;
                                                    -- P implemented by a procedure 
                                                    -- Q inherited as a null procedure
   procedure P(X: in TT3);
end PT3;
In this last case there are no entries and so we have the juxtaposition with end which is somewhat similar to the juxtaposition is end that occurs with generic packages used as signatures.
Observe how the first parameter which denotes the task is omitted if it is implemented by an entry. This echoes the new prefixed notation for calling operations of tagged types in general. Remember that rather than writing 
Op(X, Y, Z, ...);
we can write 
X.Op(Y, Z, ...);
provided certain conditions hold such as that X is of a tagged type and that Op is a primitive operation of that type.
In order for the implementation of an interface operation by an entry of a task type or a protected operation of a protected type to be possible some fairly obvious conditions must be satisfied.
In all cases the first parameter of the interface operation must be of the task type or protected type (it may be an access parameter).
In addition, in the case of a protected type, the first parameter of an operation implemented by a protected procedure or entry must have mode out or in out (and in the case of an access parameter it must be an access to variable parameter).
If the operation does not fit these rules then it has to be implemented as a subprogram. An important example is that a function has to be implemented as a function in the case of a task type because there is no such thing as a function entry. However, a function can often be directly implemented as a protected function in the case of a protected type.
Entries and protected operations which implement inherited operations may be in the visible part or private part of the task or protected type in the same way as for tagged record types.
It may seem rather odd that an operation can be implemented by a subprogram that is not part of the task or protected type itself – it seems as if it might not be task safe in some way. But a common paradigm is where an operation as an abstraction has to be implemented by two or more entry calls. An example occurs in some implementations of the classic readers and writers problem as we shall see in a moment.
Of course a task or protected type which implements an interface can have additional entries and operations as well just as a derived tagged type can have more operations than its parent.
The overriding indicators overriding and not overriding can be applied to entries as well as to procedures. Thus the package PT2 above could be written as 
package PT2 is
   task type TT2 is new TI with
      overriding    -- P implemented by an entry
      entry P;
   end TT2;
   overriding    -- Q implemented by procedure
   procedure Q(X: in TT2; I: in Integer);
end PT2;
:We will now explore a simple readers and writers example in order to illustrate various points. We start with the following interface 
package RWP is
   type RW is limited interface;
   procedure Write(Obj: out RW; X: in Item) is abstract;
   procedure Read(Obj: in RW; X: out Item) is abstract;
end RWP;
The intention here is that the interface describes the abstraction of providing an encapsulation of a hidden location and a means of writing a value (of some type Item) to it and reading a value from it – very trivial.
We could implement this in a nonsynchronized manner thus 
type Simple_RW is new RW with
   record
      V: Item;
   end record;
overriding
procedure Write(Obj: out Simple_RW; X: in Item);
overriding
procedure Read(Obj: in Simple_RW; X: out Item);
...
procedure Write(Obj: out Simple_RW; X: in Item)
is
begin
   Obj.V := X;
end Write;
procedure Read(Obj: in Simple_RW; X: out Item) is
begin
   X := Obj.V;
end Read;
This implementation is of course not task safe (task safe is sometimes referred to as thread-safe). If a task calls Write and the type Item is a composite type and the writing task is interrupted part of the way through writing, then a task which calls Read might get a curious result consisting of part of the new value and part of the old value.
For illustration we could derive a synchronized interface 
type Sync_RW is synchronized interface and RW;
This interface can only be implemented by a task or protected type. For a protected type we might have 
protected type Prot_RW is new Sync_RW with
   overriding
   procedure Write(X: in Item);
   overriding
   procedure Read(X: out Item);
private
   V: Item;
end;
protected body Prot_RW is
   procedure Write(X: in Item) is
   begin
      V := X;
   end Write;
   procedure Read(X: out Item) is
   begin
      X := V;
   end Read;
end Prot_RW;
Again observe how the first parameter of the interface operations is omitted when they are implemented by protected operations.
This implementation is perfectly task safe. However, one of the characteristics of the readers and writers example is that it is quite safe to allow multiple readers since they cannot interfere with each other. But the type Prot_RW does not allow multiple readers because protected procedures can only be executed by one task at a time.
Now consider 
protected type Multi_Prot_RW is new Sync_RW with
   overriding
   procedure Write(X: in Item);
   not overriding
   function Read return Item;
private
   V: Item;
end;
overriding
procedure Read(Obj: in Multi_Prot_RW; X: out Item);
...
protected body Multi_Prot_RW is
   procedure Write(X: in Item) is
   begin
      V := X;
   end Write;
   function Read return Item is
   begin
      return V;
   end Read;
end Multi_Prot_RW;
procedure Read(Obj: in Multi_Prot_RW; X: out Item)
is
begin
   X := Obj.Read;
end Read;
In this implementation the procedure Read is implemented by a procedure outside the protected type and this procedure then calls the function Read within the protected type. This allows multiple readers because one of the characteristics of protected functions is that multiple execution is permitted (but of course calls of the protected procedure Write are locked out while any calls of the protected function are in progress). The structure is emphasized by the use of overriding indicators.
A simple tasking implementation might be as follows 
task type Task_RW is new Sync_RW with
   overriding
   entry Write(X: in Item);
   overriding
   entry Read(X: out Item);
end;
task body Task_RW is
   V: Item;
begin
   loop
      select
         accept Write(X: in Item) do
            V := X;
         end Write;
      or
         accept Read(X: out Item) do
            X := V;
         end Read;
      or
         terminate;
      end select;
   end loop;
end Task_RW;
Finally, here is a tasking implementation which allows multiple readers and ensures that an initial value is set by only allowing a call of Write first. It is based on an example in Programming in Ada 95 by the author [6].
task type Multi_Task_RW(V: access Item) is new Sync_RW with
   overriding
   entry Write(X: in Item);
   not overriding
   entry Start;
   not overriding
   entry Stop;
end;
overriding
procedure Read(Obj: in Multi_Task_RW; X: out Item);
...
task body Multi_Task_RW is
   Readers: Integer := 0;
begin
   accept Write(X: in Item) do
      V.all := X;
   end Write;
   loop
      select
         when Write'Count = 0 =>
         accept Start;
         Readers := Readers + 1;
      or
         accept Stop;
         Readers := Readers – 1;
      or
         when Readers = 0 =>
         accept Write(X: in Item) do
            V.all := X;
         end Write;
      or
         terminate;
      end select;
   end loop;
end Multi_Task_RW;
overriding
procedure
 Read(Obj: in Multi_Task_RW; X: out Item) is
begin
   Obj.Start;
   X := Obj.V.all;
   Obj.Stop;
end Read;
In this case the data being protected is accessed via the access discriminant of the task. It is structured this way so that the procedure Read can read the data directly. Note also that the procedure Read (which is the implementation of the procedure Read of the interface) calls two entries of the task.
It should be observed that this last example is by way of illustration only. As is well known, the Count attribute used in tasks (as opposed to protected objects) can be misleading if tasks are aborted or if entry calls are timed out. Moreover, it would be gruesomely slow.
So we have seen that a limited interface such as RW might be implemented by a normal tagged type (plus its various operations) and by a protected type and also by a task type. We could then dispatch to the operations of any of these according to the tag of the type concerned. Observe that task and protected types are now other forms of tagged types and so we have to be careful to say tagged record type (or informally, normal tagged type) where appropriate.
In the above example, the types Simple_RW, Prot_RW, Multi_Prot_RW, Task_RW and Multi_Task_RW all implement the interface RW.
So we might have 
RW_Ptr: access RW'Class := ...
...
RW_Ptr.Write(An_Item);    -- dispatches
and according to the value in RW_Ptr this might call the appropriate entry or procedure of an object of any of the types implementing the interface RW.
However if we have
Sync_RW_Ptr: access Sync_RW'Class := ...
then we know that any implementation of the synchronized interface Sync_RW will be task safe because it can only be implemented by a task or protected type. So the dispatching call 
Sync_RW_Ptr.Write(An_Item);    -- task safe dispatching
will be task safe.
An interesting point is that because a dispatching call might be to an entry or to a procedure we now permit what appear to be procedure calls in timed entry calls if they might dispatch to an entry.
So we could have 
select
   RW_Ptr.Read(An_Item);    -- dispatches
or
   delay Seconds(10);
end select;
Of course it might dispatch to the procedure Read if the type concerned turns out to be Simple_RW in which case a time out could not occur. But if it dispatched to the entry Read of the type Task_RW then it could time out.
On the other hand we are not allowed to use a timed call if it is statically known to be a procedure. So 
A_Simple_Object: Simple_RW;
...
select
   A_Simple_Object.Read(An_Item);    -- illegal
or
   delay Seconds(10);
end select;
is not permitted.
A note of caution is in order. Remember that the time out is to when the call gets accepted. If it dispatches to Multi_Task_RW.Read then time out never happens because the Read itself is a procedure and gets called at once. However, behind the scenes it calls two entries and so could take a long time. But if we called the two entries directly with timed calls then we would get a time out if there were a lethargic writer in progress. So the wrapper distorts the abstraction. In a sense this is not much worse than the problem we have anyway that a time out is to when a call is accepted and not to when it returns – it could hardly be otherwise.
The same rules apply to conditional entry calls and also to asynchronous select statements where the triggering statement can be a dispatching call.
In a similar way we also permit timed calls on entries renamed as procedures. But note that we do not allow timed calls on generic formal subprograms even though they might be implemented as entries.
Another important point to note is that we can as usual assume the common properties of the class concerned. Thus in the case of a task interface we know that it must be implemented by a task and so the operations such as abort and the attributes Identity, Callable and so on can be applied. If we know that an interface is synchronized then we do know that it has to be implemented by a task or a protected type and so is task safe.
Typically an interface is implemented by a task or protected type but it can also be implemented by a singleton task or protected object despite the fact that singletons have no type name. Thus we might have 
protected An_RW is new Sync_RW with
   procedure Write(X: in Item);
   procedure Read(X: out Item);
end;
with the obvious body. However we could not declare a single protected object similar to the type Multi_Prot_RW above. This is because we need a type name in order to declare the overriding procedure Read outside the protected object. So singleton implementations are possible provided that the interface can be implemented directly by the task or protected object without external subprograms.
Here is another example
type Map is protected interface;
procedure Put(M: Map; K: Key; V: Value) is abstract;
can be implemented by
protected A_Map is new Map with
   procedure Put(K: Key; V: Value);
   ...
end A_Map;
There is an important rule about tagged private types and synchronized interfaces. Both partial and full view must be synchronized or not. Thus if we wrote 
type SI is synchronized interface;
type T is synchronized new SI with private;    -- Says synchronized
then the full type T has to be a task type or protected type or possibly a synchronized, protected or task interface.
It is vital that the synchronized property cannot be hidden since this would violate privacy. This is largely because type extensions of synchronized interfaces and tagged concurrent types are not allowed. We musn't need to look into the private part to see whether a type extension is allowed. Note that the word synchronized is always given. We could also write
type LI is limited interface;
type T is synchronized new LI with private;
in which case the ancestor is not synchronized. But the fact that T is synchronized is clearly visible.
It might be remembered that if a private view is untagged then the full view might be tagged. In this case type extension is not allowed with the private view anyway and so the full type might be synchronized. So we can have (in Ada 95 as well)
   type T is limited private;    -- untagged
private
   task type T is ...    -- synchronized property is hidden
but we cannot have
   type T is abstract tagged limited private;    -- tagged
private
   type T is synchronized interface;    -- illegal
We conclude this discussion on interfaces by saying a few words about the use of the word limited. (Much of this has already been explained in the chapter on the object oriented model (see 2.4) but it is worth repeating in the context of concurrent types.) We always explicitly insert limited, synchronized, task, or protected in the case of a limited interface in order to avoid confusion. So to derive a new explicitly limited interface from an existing limited interface LI we write 
type LI2 is limited interface and LI;
whereas in the case of normal types we can write
type LT is limited ...
type LT2 is new LT and LI with ...    -- LT2 is limited
then LT2 is limited by the normal derivation rules. Types take their limitedness from their parent (the first one in the list, provided it is not an interface) and it does not have to be given explicitly on type derivation – although it can be in Ada 2005 thus 
type LT2 is limited new LT and LI with ...
Remember the important rule that all descendants of a nonlimited interface have to be nonlimited because otherwise limited types could end up with an assignment operation.
This means that we cannot write 
type NLI is interface;    -- nonlimited
type LI is limited interface;    -- limited
task type TT is new NLI and LI with ...    --illegal
This is illegal because the interface NLI in the declaration of the task type TT is not limited.

Contents   Index   References   Search   Previous   Next 
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association and its member companies: ARA Members AdaCore Polyspace Technologies Praxis Critical Systems IBM Rational Sofcheck and   Ada-Europe:
Ada-Europe