Rationale for Ada 2005
5.6 CPU clocks and timers
Ada 2005 introduces three different kinds of timers.
Two are concerned with monitoring the CPU time of tasks – one applies
to a single task and the other to groups of tasks. The third timer measures
real time rather than execution time and can be used to trigger events
at specific real times. We will look first at the CPU timers because
that introduces more new concepts.
The execution time of one or more tasks can be monitored
and controlled by the new package
Ada.Execution_Time
plus two child packages.
Ada.Execution_Time
–
This is the root package and enables the monitoring of execution time
of individual tasks.
Ada.Execution_Time.Timers
–
This provides facilities for defining and enabling timers and for establishing
a handler which is called by the run time system when the execution time
of the task reaches a given value.
Ada.Execution_Time.Group_Budgets
–
This enables several tasks to share a budget and provides means whereby
action can be taken when the budget expires.
The execution time
of a task, or CPU time as it is commonly called, is the time spent by
the system executing the task and services on its behalf. CPU times are
represented by the private type
CPU_Time.
This type and various subprograms are declared in the root package
Ada.Execution_Time
whose specification is as follows (as before we have added some use clauses
in order to ease the presentation)
with Ada.Task_Identification; use Ada.Task_Identification;
with Ada.Real_Time; use Ada.Real_Time;
package Ada.Execution_Time is
type CPU_Time is private;
CPU_Time_First: constant CPU_Time;
CPU_Time_Last: constant CPU_Time;
CPU_Time_Unit: constant := implementation-defined-real-number;
CPU_Tick: constant Time_Span;
function Clock(T: Task_Id := Current_Task) return CPU_Time;
function "+" (Left: CPU_Time; Right: Time_Span) return CPU_Time;
function "+" (Left: Time_Span; Right: CPU_Time) return CPU_Time;
function "–" (Left: CPU_Time; Right: Time_Span) return CPU_Time;
function "–" (Left: CPU_Time; Right: CPU_Time) return Time_Span;
function "<" (Left, Right: CPU_Time) return Boolean;
function "<=" (Left, Right: CPU_Time) return Boolean;
function ">" (Left, Right: CPU_Time) return Boolean;
function ">=" (Left, Right: CPU_Time) return Boolean;
procedure Split(T: in CPU_Time; SC: out Seconds_Count; TS: out Time_Span);
function Time_Of(SC: Seconds_Count; TS: Time_Span := Time_Span_Zero)
return CPU_Time;
private
... -- not specified by the language
end Ada.Execution_Time;
The CPU time of a particular task is obtained by
calling the function Clock with the task as
parameter. It is set to zero at task creation.
The constants CPU_Time_First
and CPU_Time_Last give the range of values
of CPU_Time. CPU_Tick
gives the average interval during which successive calls of Clock
give the same value and thus is a measure of the accuracy whereas CPU_Time_Unit
gives the unit of time measured in seconds. We are assured that CPU_Tick
is no greater than one millisecond and that the range of values of CPU_Time
is at least 50 years (provided always of course that the implementation
can cope).
The various subprograms perform obvious operations
on the type CPU_Time and the type Time_Span
of the package Ada.Real_Time.
A value of type CPU_Time
can be converted to a Seconds_Count plus residual
Time_Span by the function Split
which is similar to that in the package Ada.Real_Time.
The function Time_Of similarly works in the
opposite direction. Note the default value of Time_Span_Zero
for the second parameter – this enables times of exact numbers
of seconds to be given more conveniently thus
Four_Secs: CPU_Time := Time_Of(4);
In order to find out
when a task reaches a particular CPU time we can use the facilities of
the child package
Ada.Execution_Time.Timers
whose specification
is
with System; use System;
package Ada.Execution_Time.Timers is
type Timer(T: not null access constant Task_Id) is tagged limited private;
type Timer_Handler is access protected procedure (TM: in out Timer);
Min_Handler_Ceiling: constant Any_Priority := implementation-defined;
procedure Set_Handler(TM: in out Timer; In_Time: Time_Span; Handler: Timer_Handler);
procedure Set_Handler(TM: in out Timer; At_Time: CPU_Time; Handler: Timer_Handler);
function Current_Handler(TM: Timer) return Timer_Handler;
procedure Cancel_Handler(TM: in out Timer; Cancelled: out Boolean);
function Time_Remaining(TM: Timer) return Time_Span;
Timer_Resource_Error: exception;
private
... -- not specified by the language
end Ada.Execution_Time.Timers;
The general idea is
that we declare an object of type Timer whose
discriminant identifies the task to be monitored – note the use
of not null and constant in the discriminant. We also declare
a protected procedure which takes the timer as its parameter and which
performs the actions required when the CPU_Time
of the task reaches some value. Thus to take some action (perhaps abort
for example although that would be ruthless) when the CPU_Time
of the task My_Task reaches 2.5 seconds we
might first declare
My_Timer: Timer(My_Task'Identity'Access);
Time_Max: CPU_Time := Time_Of(2, Milliseconds(500));
and then
protected Control is
procedure Alarm(TM: in out Timer);
end;
protected body Control is
procedure Alarm(TM: in out Timer) is
begin
-- abort the task
Abort_Task(TM.T.all);
end Alarm;
end Control;
Finally we set the
timer in motion by calling the procedure Set_Handler
which takes the timer, the time value and (an access to) the protected
procedure thus
Set_Handler(My_Timer, Time_Max, Control.Alarm'Access);
and then when the CPU time of the task reaches Time_Max,
the protected procedure Control.Alarm is executed.
Note how the timer object incorporates the information regarding the
task concerned using an access discriminant T
and that this is passed to the handler via its parameter TM.
Aborting the task is
perhaps a little violent. Another possibility is simply to reduce its
priority so that it is no longer troublesome, thus
-- cool that task
Set_Priority(Priority'First, TM.T.all);
Another version of Set_Handler
enables the timer to be set for a given interval (of type Time_Span).
The handler associated with a timer can be found
by calling the function Current_Handler. This
returns null if the timer is not set in which case we say that the timer
is clear.
When the timer expires,
and just before calling the protected procedure, the timer is set to
the clear state. One possible action of the handler, having perhaps made
a note of the expiration of the timer, it to set the handler again or
perhaps another handler. So we might have
protected body Control is
procedure Alarm(TM: in out Timer) is
begin
Log_Overflow(TM); -- note that timer had expired
-- and then reset it for another 500 milliseconds
Set_Handler(TM, Milliseconds(500), Kill'Access);
end Alarm;
procedure Kill(TM: in out Timer) is
begin
-- expired again so kill it
Abort_Task(TM.T.all);
end Kill;
end Control;
In this scenario we make a note of the fact that
the task has overrun and then give it another 500 milliseconds but with
the handler Control.Kill so that the second
time is the last chance.
Setting the value of
500 milliseconds directly in the call is a bit crude. It might be better
to parameterize the protected type thus
protected type Control(MS: Integer) is ...
...
My_Control: Control(500);
and then the call of
Set_Handler in the protected procedure Alarm
would be
Set_Handler(TM, Milliseconds(MS), Kill'Access);
Observe that overload resolution neatly distinguishes
whether we are calling Set_Handler with an
absolute time or a relative time.
The procedure Cancel_Handler
can be used to clear a timer. The out parameter Cancelled
is set to True if the timer was in fact set
and False if it was clear. The function Time_Remaining
returns Time_Span_Zero if the timer is not
set and otherwise the time remaining.
Note also the constant Min_Handler_Ceiling.
This is the minimum ceiling priority that the protected procedure should
have to ensure that ceiling violation cannot occur.
This timer facility might be implemented on top of
a POSIX system. There might be a limit on the number of timers that can
be supported and an attempt to exceed this limit will raise Timer_Resource_Error.
We conclude by summarizing the general principles.
A timer can be set or clear. If it is set then it has an associated (non-null)
handler which will be called after the appropriate time. The key subprograms
are Set_Handler, Cancel_Handler
and Current_Handler. The protected procedure
has a parameter which identifies the event for which it has been called.
The same protected procedure can be the handler for many events. The
same general structure applies to other kinds of timers which will now
be described.
In order to program various so-called aperiodic servers
it is necessary for tasks to share a CPU budget.
This can be done using
the child package
Ada.Execution_Time.Group_Budgets
whose specification is
with System; use System;
package Ada.Execution_Time.Group_Budgets is
type Group_Budget is tagged limited private;
type Group_Budget_Handler is access protected procedure (GB: in out Group_Budget);
type Task_Array is array (Positive range <>) of Task_Id;
Min_Handler_Ceiling: constant Any_Priority := implementation-defined;
procedure Add_Task(GB: in out Group_Budget; T: in Task_Id);
procedure Remove_Task(GB: in out Group_Budget; T: in Task_Id);
function Is_Member(GB: Group_Budget; T: Task_Id) return Boolean;
function Is_A_Group_Member(T: Task_Id) return Boolean;
function Members(GB: Group_Budget) return Task_Array;
procedure Replenish(GB: in out Group_Budget; To: in Time_Span);
procedure Add(GB: in out Group_Budget; Interval: in Time_Span);
function Budget_Has_Expired(GB: Group_Budget) return Boolean;
function Budget_Remaining(GB: Group_Budget) return Time_Span;
procedure Set_Handler(GB: in out Group_Budget; Handler: in Group_Budget_Handler);
function Current_Handler(GB: Group_Budget) return Group_Budget_Handler;
procedure Cancel_Handler(GB: in out Group_Budget; Cancelled: out Boolean);
Group_Budget_Error: exception;
private
... -- not specified by the language
end Ada.Execution_Time.Group_Budgets;
This has much in common with its sibling package
Timers but there are a number of important
differences.
The first difference is that we are here considering
a CPU budget shared among several tasks. The type Group_Budget
both identifies the group of tasks it covers and the size of the budget.
Various subprograms enable tasks in a group to be
manipulated. The procedures Add_Task and Remove_Task
add or remove a task. The function Is_Member
identifies whether a task belongs to a specific group whereas Is_A_Group_Member
identifies whether a task belongs to any group. A task cannot be a member
of more than one group. An attempt to add a task to more than one group
or remove it from the wrong group and so on raises Group_Budget_Error.
Finally the function Members returns all the
members of a group as an array.
The value of the budget (initially Time_Span_Zero)
can be loaded by the procedure Replenish and
increased by the procedure Add. Whenever a
budget is non-zero it is counted down as the tasks in the group execute
and so consume CPU time. Whenever a budget goes to Time_Span_Zero
it is said to have become exhausted and is not reduced further. Note
that Add with a negative argument can reduce
a budget – it can even cause it to become exhausted but not make
it negative.
The function Budget_Remaining
simply returns the amount left and Budget_Has_Expired
returns True if the budget is exhausted and
so has value Time_Span_Zero.
Whenever a budget becomes exhausted (that
is when the value transitions to zero) a hander is called if one has
been set. A handler is a protected procedure as before and procedures
Set_Handler, Cancel_Handler,
and function Current_Handler are much as expected.
But a major difference is that Set_Handler
does not set the time value of the budget since that is done by Replenish
and Add. The setting of the budget and the
setting of the handler are decoupled in this package. Indeed a handler
can be set even though the budget is exhausted and the budget can be
counting down even though no handler is set. The reason for the different
approach simply reflects the usage paradigm for the feature.
So we could set up
a mechanism to monitor the CPU time usage of a group of three tasks TA,
TB, and TC by first
declaring an object of type Group_Budget,
adding the three tasks to the group and then setting an appropriate handler.
Finally we call Replenish which sets the counting
mechanism going. So we might write
ABC: Group_Budget;
...
Add_Task(ABC, TA'Identity);
Add_Task(ABC, TB'Identity);
Add_Task(ABC, TC'Identity);
Set_Handler(ABC, Control.Monitor'Access);
Replenish(ABC, Seconds(10));
Remember that functions Seconds
and Minutes have been added to the package
Ada.Real_Time.
The protected procedure
might be
protected body Control is
procedure Monitor(GB: in out Group_Budget) is
begin
Log_Budget;
Add(GB, Seconds(10)); -- add more time
end Monitor;
end Control;
The procedure Monitor
logs the fact that the budget was exhausted and then adds a further 10
seconds to it. Remember that the handler remains set all the time in
the case of group budgets whereas in the case of the single task timers
it automatically becomes cleared and has to be set again if required.
If a task terminates then it is removed from the
group as part of the finalization process.
Note that again there is the constant Min_Handler_Ceiling.
The final kind of timer concerns real time rather
than CPU time and so is provided by a child package of
Ada.Real_Time
whereas the timers we have seen so far were provided by child packages
of
Ada.Execution_Time. The specification of
the package
Ada.Real_Time.Timing_Events is
package Ada.Real_Time.Timing_Events is
type Timing_Event is tagged limited private;
type Timing_Event_Handler is access protected procedure (Event: in out Timing_Event);
procedure Set_Handler(Event: in out Timing_Event; At_Time: Time;
Handler: Timing_Event_Handler);
procedure Set_Handler(Event: in out Timing_Event; In_Time: Time_Span;
Handler: Timing_Event_Handler);
function Current_Handler(Event: Timing_Event) return Timing_Event_Handler;
procedure Cancel_Handler(Event: in out Timing_Event; Cancelled: out Boolean);
function Time_Of_Event(Event: Timing_Event) return Time;
private
... -- not specified by the language
end Ada.Real_Time.Timing_Events;
This package provides a very low level facility and
does not involve Ada tasks at all. It has a very similar pattern to the
package Execution_Time.Timers. A handler can
be set by Set_Handler and again there are
two versions one for a relative time and one for absolute time. There
are also subprograms Current_Handler and Cancel_Handler.
If no handler is set then Current_Handler
returns null.
Set_Handler also specifies
the protected procedure to be called when the time is reached. Times
are of course specified using the type Real_Time
rather than CPU_Time.
A minor difference is that this package has a function
Time_Of_Event rather than Time_Remaining.
A simple example was
given in the introductory chapter. We repeat it here for convenience.
The idea is that we wish to ring a pinger when our egg is boiled after
four minutes. The protected procedure might be
protected body Egg is
procedure Is_Done(Event: in out Timing_Event) is
begin
Ring_The_Pinger;
end Is_Done;
end Egg;
and then
Egg_Done: Timing_Event;
Four_Min: Time_Span := Minutes(4);
...
Put_Egg_In_Water;
Set_Handler(Event => Egg_Done, In_Time => Four_Min, Handler => Egg.Is_Done'Access);
-- now read newspaper whilst waiting for egg
This is unreliable
because if we are interrupted between the calls of Put_Egg_In_Water
and Set_Handler then the egg will be boiled
for too long. We can overcome this by adding a further protected procedure
Boil to the protected object and placing Is_Done
in the private part so that it becomes
protected Egg is
procedure Boil(For_Time: in Time_Span);
private
procedure Is_Done(Event: in out Timing_Event);
Egg_Done: Timing_Event;
end Egg;
protected body Egg is
procedure Boil(For_Time: in Time_Span) is
begin
Put_Egg_In_Water;
Set_Handler(Egg_Done, For_Time, Is_Done'Access);
end Boil;
procedure Is_Done(Event: in out Timing_Event) is
begin
Ring_The_Pinger;
end Is_Done;
end Egg;
This is much better.
The timing mechanism is now completely encapsulated in the protected
object and the procedure Is_Done is no longer
visible outside. So all we have to do is
Egg.Boil(Minutes(4));
-- now read newspaper whilst waiting for egg
Of course if the telephone rings as the pinger goes
off and before we have a chance to eat the egg then it still gets overdone.
One solution is to eat the egg within the protected procedure
Is_Done
as well. A gentleman would never let a telephone call disturb his breakfast.
One protected procedure could be used to respond
to several events. In the case of the CPU timer the discriminant of the
parameter identifies the task; in the case of the group and real-time
timers, the parameter identifies the event.
If we want to use the same timer for several events
then various techniques are possible. Note that the timers are limited
so we cannot test for them directly. However, they are tagged and so
can be extended. Moreover, we know that they are passed by reference
and that the parameters are considered aliased.
Suppose we are boiling
six eggs in one of those French breakfast things with a different coloured
holder for each egg. We can write
type Colour is (Black, Blue, Red, Green, Yellow, Purple);
Eggs_Done: array (Colour) of aliased Timing_Event;
We can then set the
handler for the egg in the red holder by something like
Set_Handler(Eggs_Done(Red), For_Time, Is_Done'Access);
and then the protected
procedure might be
procedure Is_Done(E: in out Timing_Event) is
begin
for C in Colour loop
if E'Access = Eggs_Done(C)'Access then
-- egg in holder colour C is ready
...
return;
end if;
end loop;
-- falls out of loop – unknown event!
raise Not_An_Egg ;
end Is_Done;
Although this does work it is more than a little
distasteful to compare access values in this way and moreover requires
a loop to see which event occurred.
A much better approach
is to use type extension and view conversions. First we extend the type
Timing_Event to include additional information
about the event (in this case the colour) so that we can identify the
particular event from within the handler
type Egg_Event is new Timing_Event with
record
Event_Colour: Colour;
end record;
We then declare an
array of these extended events (they need not be aliased)
Eggs_Done: array (Colour) of Egg_Event;
We can now call Set_Handler
for the egg in the red holder
Set_Handler(Eggs_Done(Red), For_Time, Is_Done'Access);
This is actually a call on the Set_Handler
for the type Egg_Event inherited from Timing_Event.
But it is the same code anyway.
Remember that values of tagged types are always passed
by reference. This means that from within the procedure Is_Done
we can recover the underlying type and so discover the information in
the extension. This is done by using view conversions.
In fact we have to
use two view conversions, first we convert to the class wide type Timing_Event'Class
and then to the specific type Egg_Event. And
then we can select the component Event_Colour.
In fact we can do these operations in one statement thus
procedure Is_Done(E: in out Timing_Event) is
C: constant Colour := Egg_Event(Timing_Event'Class(E)).Event_Colour;
begin
-- egg in holder colour C is ready
...
end Is_Done;
Note that there is
a check on the conversion from the class wide type Timing_Event'Class
to the specific type Egg_Event to ensure that
the object passed as parameter is indeed of the type Egg_Event
(or a further extension of it). If this fails then Tag_Error
is raised. In order to avoid this possibility we can use a membership
test. For example
procedure Is_Done(E: in out Timing_Event) is
C: Colour;
begin
if Timing_Event'Class(E) in Egg_Event then
C := Egg_Event(Timing_Event'Class(E)).Event_Colour;
-- egg in holder colour C is ready
...
else
-- unknown event – not an egg event!
raise Not_An_Egg;
end if;
end Is_Done;
The membership test ensures that the event is of
the specific type Egg_Event. We could avoid
the double conversion to the class wide type by introducing an intermediate
variable.
It is important to appreciate that no dispatching
is involved in these operations at all – everything is static apart
from the membership test.
Of course, it would have been a little more flexible
if the various subprograms took a parameter of type Timing_Event'Class
but this would have conflicted with the Restrictions
identifier No_Dispatch. Note that Ravenscar
itself does not impose No_Dispatch but the
restriction is in the High-Integrity annex and thus might be imposed
on some high-integrity applications which might nevertheless wish to
use timers in a simple manner.
A few minor points of difference between the timers
are worth summarizing.
The two CPU timers have a constant Min_Handler_Ceiling.
This prevents ceiling violation. It is not necessary for the real-time
timer because the call of the protected procedure is treated like an
interrupt and thus is at interrupt ceiling level.
The group budget timer and the real-time timer do
not have an exception corresponding to Timer_Resource_Error
for the single task CPU timer. As mentioned above, it is anticipated
that the single timer might be implemented on top of a POSIX system in
which case there might be a limit to the number of timers especially
since each task could be using several timers. In the group case, a task
can only be in one group so the number of group timers is necessarily
less than the number of tasks and no limit is likely to be exceeded.
In the real-time case the events are simply placed on the delay queue
and no other resources are required anyway.
It should also be noted that the group timer could
be used to monitor the execution time of a single task. However, a task
can only be in one group and so only one timer could be applied to a
task that way whereas, as just mentioned, the single CPU timer is quite
different since a given task could have several timers set for it to
expire at different times. Thus both kinds of timers have their own distinct
usage patterns.
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by: