Rationale for Ada 2005
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.
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by: