Rationale for Ada 2005: Tasking and Real-Time

RUSTOP
BACKNEXT

ENG

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)

  1        with Ada.Task_Identification; use Ada.Task_Identification;
  2        with Ada.Real_Time; use Ada.Real_Time;
  3        package Ada.Execution_Time is
  4                type CPU_Time is private;
  5                CPU_Time_First : constant CPU_Time;
  6                CPU_Time_Last  : constant CPU_Time;
  7                CPU_Time_Unit  : constant := implementation-defined-real-number;
  8                CPU_Tick       : constant Time_Span;
  9                function Clock (T : Task_Id := Current_Task) return CPU_Time;
 10                function "+"  (Left : CPU_Time; Right : Time_Span) return CPU_Time;
 11                function "+"  (Left : Time_Span; Right : CPU_Time) return CPU_Time;
 12                function "–"  (Left : CPU_Time; Right : Time_Span) return CPU_Time;
 13                function "–"  (Left : CPU_Time; Right : CPU_Time) return Time_Span;
 14                function "<"  (Left, Right : CPU_Time) return Boolean;
 15                function "<=" (Left, Right : CPU_Time) return Boolean;
 16                function ">"  (Left, Right : CPU_Time) return Boolean;
 17                function ">=" (Left, Right : CPU_Time) return Boolean;
 18                procedure Split (T : in CPU_Time; SC : out Seconds_Count; TS : out Time_Span);
 19                function Time_Of (SC : Seconds_Count; TS : Time_Span := Time_Span_Zero) return CPU_Time;
 20        private
 21                ... -- not specified by the language
 22        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

  1        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

  1        with System; use System;
  2        package Ada.Execution_Time.Timers is
  3                type Timer (T : not null access constant Task_Id) is tagged limited private;
  4                type Timer_Handler is access protected procedure (TM : in out Timer);
  5                Min_Handler_Ceiling : constant Any_Priority := implementation-defined;
  6                procedure Set_Handler (TM : in out Timer; In_Time : Time_Span; Handler : Timer_Handler);
  7                procedure Set_Handler (TM : in out Timer; At_Time : CPU_Time; Handler : Timer_Handler);
  8                function Current_Handler (TM : Timer) return Timer_Handler;
  9                procedure Cancel_Handler (TM : in out Timer; Cancelled : out Boolean);
 10                function Time_Remaining (TM : Timer) return Time_Span;
 11                Timer_Resource_Error : exception;
 12        private
 13                ... -- not specified by the language
 14        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

  1        My_Timer : Timer (My_Task'Identity'Access);
  2        Time_Max : CPU_Time := Time_Of (2, Milliseconds (500));

@ and then

  1        protected Control is
  2                procedure Alarm (TM : in out Timer);
  3        end;
  4        protected body Control is
  5                procedure Alarm (TM : in out Timer) is
  6                begin
  7                        -- abort the task
  8                        Abort_Task (TM.T.all);
  9                end Alarm;
 10        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

  1        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

  1        -- cool that task
  2        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

  1        protected body Control is
  2                procedure Alarm (TM : in out Timer) is
  3                begin
  4                        Log_Overflow (TM); -- note that timer had expired
  5                        -- and then reset it for another 500 milliseconds
  6                        Set_Handler (TM, Milliseconds (500), Kill'Access);
  7                end Alarm;
  8                procedure Kill (TM : in out Timer) is
  9                begin
 10                        -- expired again so kill it
 11                        Abort_Task (TM.T.all);
 12                end Kill;
 13        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

  1        protected type Control (MS : Integer) is ...
  2        ...
  3        My_Control : Control (500);

@ and then the call of Set_Handler in the protected procedure Alarm would be

  1        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

  1        with System; use System;
  2        package Ada.Execution_Time.Group_Budgets is
  3                type Group_Budget is tagged limited private;
  4                type Group_Budget_Handler is access protected procedure (GB : in out Group_Budget);
  5                type Task_Array is array (Positive range <>) of Task_Id;
  6                Min_Handler_Ceiling : constant Any_Priority := implementation-defined;
  7                procedure Add_Task (GB : in out Group_Budget; T : in Task_Id);
  8                procedure Remove_Task (GB : in out Group_Budget; T: in Task_Id);
  9                function Is_Member (GB : Group_Budget; T : Task_Id) return Boolean;
 10                function Is_A_Group_Member (T : Task_Id) return Boolean;
 11                function Members (GB : Group_Budget) return Task_Array;
 12                procedure Replenish (GB : in out Group_Budget; To : in Time_Span);
 13                procedure Add (GB : in out Group_Budget; Interval : in Time_Span);
 14                function Budget_Has_Expired (GB : Group_Budget) return Boolean;
 15                function Budget_Remaining (GB : Group_Budget) return Time_Span;
 16                procedure Set_Handler (GB : in out Group_Budget; Handler : in Group_Budget_Handler);
 17                function Current_Handler (GB : Group_Budget) return Group_Budget_Handler;
 18                procedure Cancel_Handler (GB : in out Group_Budget; Cancelled : out Boolean);
 19                Group_Budget_Error : exception;
 20        private
 21                ... -- not specified by the language
 22        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

  1        ABC: Group_Budget;
  2        ...
  3        Add_Task (ABC, TA'Identity);
  4        Add_Task (ABC, TB'Identity);
  5        Add_Task (ABC, TC'Identity);
  6        Set_Handler (ABC, Control.Monitor'Access);
  7        Replenish (ABC, Seconds (10));

@ Remember that functions Seconds and Minutes have been added to the package Ada.Real_Time.

@ The protected procedure might be

  1        protected body Control is
  2                procedure Monitor (GB : in out Group_Budget) is
  3                begin
  4                        Log_Budget;
  5                        Add (GB, Seconds (10)); -- add more time
  6                end Monitor;
  7        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

  1        package Ada.Real_Time.Timing_Events is
  2                type Timing_Event is tagged limited private;
  3                type Timing_Event_Handler is access protected procedure (Event : in out Timing_Event);
  4                procedure Set_Handler (Event : in out Timing_Event; At_Time : Time; Handler : Timing_Event_Handler);
  5                procedure Set_Handler (Event : in out Timing_Event; In_Time : Time_Span; Handler : Timing_Event_Handler);
  6                function Is_Handler_Set (Event : Timing_Event) return Boolean;
  7                function Current_Handler (Event : Timing_Event) return Timing_Event_Handler;
  8                procedure Cancel_Handler (Event : in out Timing_Event; Cancelled : out Boolean);
  9                function Time_Of_Event (Event : Timing_Event) return Time;
 10        private
 11                ... -- not specified by the language
 12        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 paper. 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

  1        protected body Egg is
  2                procedure Is_Done (Event : in out Timing_Event) is
  3                begin
  4                        Ring_The_Pinger;
  5                end Is_Done;
  6        end Egg;

@ and then

  1        Egg_Done : Timing_Event;
  2        Four_Min : Time_Span := Minutes (4);
  3        ...
  4        Put_Egg_In_Water;
  5        Set_Handler (Event => Egg_Done, In_Time => Four_Min, Handler => Egg.Is_Done'Access);
  6        -- 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 procedure to the protected object so that it becomes

  1        protected Egg is
  2                procedure Boil (For_Time : in Time_Span);
  3                procedure Is_Done (Event : in out Timing_Event);
  4        end Egg;
  5        protected body Egg is
  6                Egg_Done : Timing_Event;
  7                procedure Boil (For_Time : in Time_Span) is
  8                begin
  9                        Put_Egg_In_Water;
 10                        Set_Handler (Egg_Done, For_Time, Is_Done'Access);
 11                end Boil;
 12                procedure Is_Done (Event : in out Timing_Event) is
 13                begin
 14                        Ring_The_Pinger;
 15                end Is_Done;
 16        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

  1        Egg.Boil (Minutes (4));
  2        -- 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

  1        type Colour is (Black, Blue, Red, Green, Yellow, Purple);
  2        Eggs_Done : array (Colour) of aliased Timing_Event;

@ We can then set the handler for the egg in the red holder by something like

  1        Set_Handler (Eggs_Done (Red), For_Time, Is_Done'Access);

@ and then the protected procedure might be

  1        procedure Is_Done (E : in out Timing_Event) is
  2        begin
  3                for C in Colour loop
  4                        if E'Access = Eggs_Done(C)'Access then
  5                                -- egg in holder colour C is ready
  6                                ...
  7                                return;
  8                        end if;
  9                end loop;
 10                -- falls out of loop – unknown event!
 11                raise Not_An_Egg;
 12        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

  1        type Egg_Event is new Timing_Event with
  2                record
  3                        Event_Colour : Colour;
  4                end record;

@ We then declare an array of these extended events (they need not be aliased)

  1        Eggs_Done : array (Colour) of Egg_Event;

@ We can now call Set_Handler for the egg in the red holder

  1        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

  1        procedure Is_Done (E : in out Timing_Event) is
  2                C : constant Colour := Egg_Event (Timing_Event'Class (E)).Event_Colour;
  3        begin
  4                -- egg in holder colour C is ready
  5                ...
  6        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

  1        procedure Is_Done (E : in out Timing_Event) is
  2                C : Colour;
  3        begin
  4                if Timing_Event'Class (E) in Egg_Event then
  5                        C := Egg_Event (Timing_Event'Class (E)).Event_Colour;
  6                        -- egg in holder colour C is ready
  7                        ...
  8                else
  9                        -- unknown event – not an egg event!
 10                        raise Not_An_Egg;
 11                end if;
 12        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.

Rationale for Ada 2005: Tasking and Real-Time

@ENGRUSTOPBACKNEXT

6. Процессорное время и таймеры

@ Ада 2005 вводит три различных вида таймеров. Два из них связаны с контролем процессорного времени задач - первый из которых применяется к единственной задаче, другой к группам задач. Третий таймер измеряет реальное время, а не время выполнения и может использоваться, чтобы вызвать события в определенный момент реального времени. Мы сначала рассмотрим таймеры центрального процессора, потому что там вводится много новых понятий.

@ Время выполнения одной или более задач может отслеживаться и управляться новым пакетом Ada.Execution_Time вместе с двумя своими дочерними пакетами.

@ Дочерний пакет Ада. Execution_Time - это корневой пакет осуществляющий контроль времени выполнения индивидуальных задач.

@ Дочерний пакет Ада.Execution_Time.Timers обеспечивает средства для определения параметров и запуска таймеров и обработчиков события когда время выполнения задачи достигает данного значения.

@ Пакет Ada.Execution_Time.Group_Budgets позволяет нескольким задачам совместно использовать бюджет и определяет действие которое может быть предпринято, когда бюджет истекает.

@ Время выполнения задачи, или процессорное время, как его обычно называют, является время затраченное системой для выполнения задачи и службы от своего лица. Процессорные времена представлены приватным типом CPU_Time. Этот тип и различные подпрограммы объявлены в корневом пакете Ada.Execution_Time, спецификация которого следующая (как и прежде, мы добавили некоторые выражения использования, чтобы повысить читабельгость):

  1        with Ada.Task_Identification; use Ada.Task_Identification;
  2        with Ada.Real_Time; use Ada.Real_Time;
  3        package Ada.Execution_Time is
  4                type CPU_Time is private;
  5                CPU_Time_First : constant CPU_Time;
  6                CPU_Time_Last  : constant CPU_Time;
  7                CPU_Time_Unit  : constant := implementation-defined-real-number;
  8                CPU_Tick       : constant Time_Span;
  9                function Clock (T : Task_Id := Current_Task) return CPU_Time;
 10                function "+"  (Left : CPU_Time; Right : Time_Span) return CPU_Time;
 11                function "+"  (Left : Time_Span; Right : CPU_Time) return CPU_Time;
 12                function "–"  (Left : CPU_Time; Right : Time_Span) return CPU_Time;
 13                function "–"  (Left : CPU_Time; Right : CPU_Time) return Time_Span;
 14                function "<"  (Left, Right : CPU_Time) return Boolean;
 15                function "<=" (Left, Right : CPU_Time) return Boolean;
 16                function ">"  (Left, Right : CPU_Time) return Boolean;
 17                function ">=" (Left, Right : CPU_Time) return Boolean;
 18                procedure Split (T : in CPU_Time; SC : out Seconds_Count; TS : out Time_Span);
 19                function Time_Of (SC : Seconds_Count; TS : Time_Span := Time_Span_Zero) return CPU_Time;
 20        private
 21                ... -- not specified by the language
 22        end Ada.Execution_Time;

@ Процессорное время специфической задачи может быть получено вызовом функции Clock с задачей в качестве параметра. Оно установлено в нуль при создании задачи.

@ Константы CPU_Time_First и CPU_Time_Last дают диапазон значений CPU_Time.

@ CPU_Tick дает средний интервал во время которого последовательные вызовы Clock дают одно и то же значение, и таким образом, это точность с которой CPU_Time_Unit дает еденицы времени, измеренного в секундах. Мы уверены, что CPU_Tick не больше чем одна миллисекунда и что диапазон значений CPU_Time составляет по крайней мере 50 лет (определяется реализацией системы).

@ Различные подпрограммы выполняют очевидные операции с типом CPU_Time и типом Time_Span пакета Ada.Real_Time.

@ Значение типа CPU_Time может быть преобразовано к Seconds_Count плюс остаточный Time_Span функцией Split, которая подобна аналогичной из пакета Ada.Real_Time. Функция Time_Of так же работает в противоположном направлении. Отметим значение по умолчанию Time_Span_Zero для второго параметра - это позволяет временам точных чисел секунд быть данными более удобно таким образом:

  1        Four_Secs : CPU_Time := Time_Of (4);

@ Чтобы узнать, когда задача достигает специфического процессорного времени, мы можем использовать средства дочернего пакета Ada.Execution_Time.Timers спецификация которого:

  1        with System; use System;
  2        package Ada.Execution_Time.Timers is
  3                type Timer (T : not null access constant Task_Id) is tagged limited private;
  4                type Timer_Handler is access protected procedure (TM : in out Timer);
  5                Min_Handler_Ceiling : constant Any_Priority := implementation-defined;
  6                procedure Set_Handler (TM : in out Timer; In_Time : Time_Span; Handler : Timer_Handler);
  7                procedure Set_Handler (TM : in out Timer; At_Time : CPU_Time; Handler : Timer_Handler);
  8                function Current_Handler (TM : Timer) return Timer_Handler;
  9                procedure Cancel_Handler (TM : in out Timer; Cancelled : out Boolean);
 10                function Time_Remaining (TM : Timer) return Time_Span;
 11                Timer_Resource_Error : exception;
 12        private
 13                ... -- not specified by the language
 14        end Ada.Execution_Time.Timers;

@ Основная идея состои в том, что мы объявляем объект типа Timer дискриминант которого идентифицирует задачу, которая будет отслеживаться (отметим использование конструкций not null и constant в дискриминанте). Мы также объявляем защищенную процедуру, которая берет таймер в качестве параметра и которая будет запущена, когда CPU_Time задачи иссякнет. Таким образом, чтобы предпринять некоторое действие (возможно abort например, хотя это было бы безжалостно), когда CPU_Time задачи My_Task достигает 2.5 секунд, мы могли бы сначала объявить:

  1        My_Timer : Timer (My_Task'Identity'Access);
  2        Time_Max : CPU_Time := Time_Of (2, Milliseconds (500));

@ и тогда:

  1        protected Control is
  2                procedure Alarm (TM : in out Timer);
  3        end;
  4        protected body Control is
  5                procedure Alarm (TM : in out Timer) is
  6                begin
  7                        -- abort the task
  8                        Abort_Task (TM.T.all);
  9                end Alarm;
 10        end Control;

@ Наконец мы приводим таймер в движение вызывая процедуру Set_Handler, которая берет таймер, значение времени и ссылку на защищенную процедуру таким образом:

  1        Set_Handler (My_Timer, Time_Max, Control.Alarm'Access);

@ и затем когда процессорное время задачи достигает Time_Max выполняется защищенная процедура Control.Alarm. Отметьте, как объект таймера включает информацию относительно соответствующей задачи используя ссылочный дискриминант T и что его передают в обработчик через его параметр ТМ.

@ Прерывание задачи возможно немного сильно. Другая возможность состоит в том, чтобы просто уменьшить свой приоритет так, чтобы задача больше не выполнялась, таким образом:

  1        -- cool that task
  2        Set_Priority (Priority'First, TM.T.all);

@ Другая версия Set_Handler позволяет таймеру быть установленным для данного интервала (типа Time_Span).

@ Обработчик, связанный с таймером, может быть получен вызовом функции Current_Handler. Она возвращает пустой указатель, если таймер не установлен, тогда мы говорим, что таймер чист.

@ Когда таймер истекает, и прежде, чем вызвать защищенную процедуру, таймер устанавливается в состояние "чист". Одно возможное действие обработчика при истечении таймера это установить обработчик снова или возможно другой обработчик. Таким образом, мы могли бы иметь:

  1        protected body Control is
  2                procedure Alarm (TM : in out Timer) is
  3                begin
  4                        Log_Overflow (TM); -- note that timer had expired
  5                        -- and then reset it for another 500 milliseconds
  6                        Set_Handler (TM, Milliseconds (500), Kill'Access);
  7                end Alarm;
  8                procedure Kill (TM : in out Timer) is
  9                begin
 10                        -- expired again so kill it
 11                        Abort_Task (TM.T.all);
 12                end Kill;
 13        end Control;

@ В этом сценарии мы обращаем внимание на факт, что задача завершена и затем даём еще 500 миллисекунд, но уже с обработчиком Control.Kill который окончательно прекратит выполнение задачи.

@ Установка значения 500 миллисекунд непосредственно в вызове является немного грубоватым. Было бы лучше параметризовать защищенный тип таким образом:

  1        protected type Control (MS : Integer) is ...
  2        ...
  3        My_Control : Control (500);

@ и затем вызов Set_Handler в защищенной процедуре Alarm был бы:

  1        Set_Handler (TM, Milliseconds (MS), Kill'Access);

@ Заметим, что разрешающая способность перегрузки аккуратно различает, вызываем ли мы Set_Handler с абсолютным временем или относительным временем.

@ Процедура Cancel_Handler может использоваться, чтобы очистить таймер. Функция выдаёт True если таймер был фактически установлен и False, если он находится в очищеном состоянии. Функция Time_Remaining возвращает Time_Span_Zero, если таймер не установлен и иначе возвращает оставшееся время.

@ Отметим также констату Min_Handler_Ceiling. Это - минимальный приоритет потолка который защищённая процедура гарантирует, что потолок нарушения не может произойти.

@ Это средство таймера может быть осуществлено под управлением системы POSIX. Может быть предел количества таймеров, которые могут быть поддержаны, и попытка превысить этот предел вызывает исключение Timer_Resource_Error.

@ В заключении суммируем общие принципы. Таймер может быть установлен или быть очищенным. Если он установлено тогда, у этого есть связанный (ненулевой) обработчик, который будет вызван после подходящего времени. Ключевые подпрограммы - Set_Handler, Cancel_Handler и Current_Handler. У защищенной процедуры есть параметр, который идентифицирует событие при котором она вызвана. Та же самая защищенная процедура может быть обработчиком для многих событий. Та же самая общая структура относится к другим видам таймеров, которые будут здесь описаны.

@ Чтобы программировать различные так называемые апериодические серверы, это необходимо для задач совместно использующих бюджет центрального процессора.

@ Это может быть сделано, используя дочерний пакет Ada.Execution_Time.Group_Budgets, спецификация которого:

  1        with System; use System;
  2        package Ada.Execution_Time.Group_Budgets is
  3                type Group_Budget is tagged limited private;
  4                type Group_Budget_Handler is access protected procedure (GB : in out Group_Budget);
  5                type Task_Array is array (Positive range <>) of Task_Id;
  6                Min_Handler_Ceiling : constant Any_Priority := implementation-defined;
  7                procedure Add_Task (GB : in out Group_Budget; T : in Task_Id);
  8                procedure Remove_Task (GB : in out Group_Budget; T: in Task_Id);
  9                function Is_Member (GB : Group_Budget; T : Task_Id) return Boolean;
 10                function Is_A_Group_Member (T : Task_Id) return Boolean;
 11                function Members (GB : Group_Budget) return Task_Array;
 12                procedure Replenish (GB : in out Group_Budget; To : in Time_Span);
 13                procedure Add (GB : in out Group_Budget; Interval : in Time_Span);
 14                function Budget_Has_Expired (GB : Group_Budget) return Boolean;
 15                function Budget_Remaining (GB : Group_Budget) return Time_Span;
 16                procedure Set_Handler (GB : in out Group_Budget; Handler : in Group_Budget_Handler);
 17                function Current_Handler (GB : Group_Budget) return Group_Budget_Handler;
 18                procedure Cancel_Handler (GB : in out Group_Budget; Cancelled : out Boolean);
 19                Group_Budget_Error : exception;
 20        private
 21                ... -- not specified by the language
 22        end Ada.Execution_Time.Group_Budgets;

@ Здесь имеется много общего с братским пакетом Timers, но есть много важных различий.

@ Первое различие состоит в том, что мы здесь считаем бюджет центрального процессора разделенным среди нескольких задач. Тип Group_Budget идентифицирует группу задач и размер бюджета.

@ Различные подпрограммы позволяют управлять задачами в группе. Процедуры Add_Task и Remove_Task добавляют или удаляют задачу. Функция Is_Member идентифицирует, принадлежит ли задача определенной группе, тогда как Is_A_Group_Member идентифицирует, принадлежит ли задача любой группе. Задача не может быть членом больше чем одной группы. Попытка добавить задачу к больше чем одной группе или удалить ее из неправильной группы и так далее вызывает исключение Group_Budget_Error. Наконец, функция Members возвращает всех членов группы как массив.

@ Значение бюджета (первоначально Time_Span_Zero) может быть загружено процедурой Replenish и увеличено процедурой Add. Всякий раз, когда бюджет не является нулевым, он считается в обратном порядке, поскольку задачи в группе выполняются и так бы употребляют процессорное время. Всякий раз, когда бюджет уменьшается до значения Time_Span_Zero, и как говорят, становится исчерпанным и не может быть уменьшен далее. Отметим, что Add с отрицательным параметром, может уменьшить бюджет так, что может даже заставить его стать исчерпанным, но не сделать его негативным.

@ Функция Budget_Remaining просто возвращает оставшееся количество, и Budget_Has_Expired возвращает True, если бюджет исчерпан и значение Time_Span_Zero - также.

@ Всякий раз, когда бюджет становится опустошенным (это когда значение близко к нулю) вызывается обработчик, если он был установлен. Обработчик - защищенная процедура как и прежде, и процедуры Set_Handler, Cancel_Handler, и функция Current_Handler такие же как и раньше. Но главное различие в том, что Set_Handler не устанавливает значение времени бюджета, так как это делается посредством Replenish и Add. Урегулирование бюджета и урегулирование обработчика расцеплены в этом пакете. Действительно, обработчик может быть установлен даже при том, что бюджет исчерпан, и бюджет может считать в обратном порядке даже при том, что никакой обработчик не установлен. Причина для различного подхода просто отражает парадигму использования этой особенности.

@ Таким образом, мы можем установить механизм для контроля использования процессорного времени группой из трех задач TA, TB, и TC первым объявлением объекта типа Group_Budget, добавлением этих трех задач к группе и затем установкой соответствующего обработчика. Наконец, мы вызываем Replenish, который запускает механизм подсчета:

  1        ABC: Group_Budget;
  2        ...
  3        Add_Task (ABC, TA'Identity);
  4        Add_Task (ABC, TB'Identity);
  5        Add_Task (ABC, TC'Identity);
  6        Set_Handler (ABC, Control.Monitor'Access);
  7        Replenish (ABC, Seconds (10));

@ Напомним, что функции Seconds и Minutes были добавлены к пакету Ada.Real_Time.

@ Защищенная процедура могла бы быть:

  1        protected body Control is
  2                procedure Monitor (GB : in out Group_Budget) is
  3                begin
  4                        Log_Budget;
  5                        Add (GB, Seconds (10)); -- add more time
  6                end Monitor;
  7        end Control;

@ Процедура Monitor регистрирует факт, что бюджет был исчерпан и затем добавляет дополнительные 10 секунд в бюджет. Напомним, что обработчик остается установленным все время в случае группового бюджета, тогда как в случае единственной задачи таймер автоматически становится чистым и должен быть установлен снова если это необходимо.

@ Если задача заканчивается тогда она удаляется из группы как часть процесса завершения.

@ Отметим, что тут также имеется константа Min_Handler_Ceiling.

@ Последний вид таймеров касающегося реального а не процессорного времени предоставлен дочерним пакетом Ada.Real_Time, в то время как другие таймеры пока были предоставлены дочерними пакетами пакета Ada.Execution_Time. Спецификация пакета Ada.Real_Time.Timing_Events:

  1        package Ada.Real_Time.Timing_Events is
  2                type Timing_Event is tagged limited private;
  3                type Timing_Event_Handler is access protected procedure (Event : in out Timing_Event);
  4                procedure Set_Handler (Event : in out Timing_Event; At_Time : Time; Handler : Timing_Event_Handler);
  5                procedure Set_Handler (Event : in out Timing_Event; In_Time : Time_Span; Handler : Timing_Event_Handler);
  6                function Is_Handler_Set (Event : Timing_Event) return Boolean;
  7                function Current_Handler (Event : Timing_Event) return Timing_Event_Handler;
  8                procedure Cancel_Handler (Event : in out Timing_Event; Cancelled : out Boolean);
  9                function Time_Of_Event (Event : Timing_Event) return Time;
 10        private
 11                ... -- not specified by the language
 12        end Ada.Real_Time.Timing_Events;

@ Этот пакет обеспечивает средство очень низкого уровня и не вовлекает задачи Ады вообще. У него есть очень похожий шаблон к пакету Execution_Time.Timers. Обработчик может быть установлен процедурой Set_Handler которая имеет две версии: первая, в относительном времени и другая, в абсолютном времени. Есть также подпрограммы Current_Handler и Cancel_Handler. Если никакой обработчик не установлен тогда Current_Handler возвращает пустой указатель.

@ Set_Handler также определяет защищенную процедуру, которая будет вызвана когда время будет исчерпано. Таймеры определяются при помощи типа Real_Time, а не CPU_Time.

@ Незначительное различие состоит в том, что у этого пакета есть функция Time_Of_Event, а не Time_Remaining.

@ Простой пример был описан в вводной статье. Мы повторяем это здесь для удобства. Идея состоит в том, что мы желаем зазвонить в pinger когда наше яйцо будет варится больше четырех минут. Защищенная процедура могла бы выглядеть так:

  1        protected body Egg is
  2                procedure Is_Done (Event : in out Timing_Event) is
  3                begin
  4                        Ring_The_Pinger;
  5                end Is_Done;
  6        end Egg;

@ и тогда:

  1        Egg_Done : Timing_Event;
  2        Four_Min : Time_Span := Minutes (4);
  3        ...
  4        Put_Egg_In_Water;
  5        Set_Handler (Event => Egg_Done, In_Time => Four_Min, Handler => Egg.Is_Done'Access);
  6        -- now read newspaper whilst waiting for egg

@ Это ненадежно, потому что, если мы будем прерваны между вызовами Put_Egg_In_Water и Set_Handler тогда яйцо будет вариться слишком долго. Мы можем исправить дело добавив дальнейшую процедуру к защищенному объекту так, чтобы это стало:

  1        protected Egg is
  2                procedure Boil (For_Time : in Time_Span);
  3                procedure Is_Done (Event : in out Timing_Event);
  4        end Egg;
  5        protected body Egg is
  6                Egg_Done : Timing_Event;
  7                procedure Boil (For_Time : in Time_Span) is
  8                begin
  9                        Put_Egg_In_Water;
 10                        Set_Handler (Egg_Done, For_Time, Is_Done'Access);
 11                end Boil;
 12                procedure Is_Done (Event : in out Timing_Event) is
 13                begin
 14                        Ring_The_Pinger;
 15                end Is_Done;
 16        end Egg;

@ Это уже гораздо лучше. Механизм выбора времени теперь полностью формируется в защищенном объекте и процедура Is_Done больше не видима снаружи. Таким образом, все что мы должны сделать:

  1        Egg.Boil (Minutes (4));
  2        -- now read newspaper whilst waiting for egg

@ Конечно, если звонок pinger закончится раньше чем у нас будет возможность съесть яйцо тогда, это все еще становится преувеличенным. Одно из решений состоит в том, чтобы съесть яйцо в пределах защищенной процедуры Is_Done. Действительно, джентльмен никогда не позволял бы телефонному звонку нарушать его завтрак.

@ Одна защищенная процедура может использоваться, чтобы ответить на несколько событий. В случае таймера центрального процессора дискриминант параметра идентифицирует задачу; в случае группы и таймеров в реальном времени, параметр идентифицирует событие.

@ Если мы хотим использовать тот же самый таймер для нескольких событий тогда, возможны различные методики. Отметим, что таймеры ограничены так, мы не можем проверить их непосредственно. Однако, т.к. они являются тегового типа то они могут быть быть соответственно расширены. Кроме того, мы знаем, что они передаются по ссылке и что параметры считаются aliased.

@ Предположим, что мы кипятим шесть яиц в одной из тех французских вещей для завтрака с разноцветным держателем для каждого яйца. Мы можем написать:

  1        type Colour is (Black, Blue, Red, Green, Yellow, Purple);
  2        Eggs_Done : array (Colour) of aliased Timing_Event;

@ Мы можем тогда установить обработчик для яйца в красном держателе:

  1        Set_Handler (Eggs_Done (Red), For_Time, Is_Done'Access);

@ и затем защищенная процедура могла бы быть:

  1        procedure Is_Done (E : in out Timing_Event) is
  2        begin
  3                for C in Colour loop
  4                        if E'Access = Eggs_Done(C)'Access then
  5                                -- egg in holder colour C is ready
  6                                ...
  7                                return;
  8                        end if;
  9                end loop;
 10                -- falls out of loop – unknown event!
 11                raise Not_An_Egg;
 12        end Is_Done;

@ Хотя это и работает, но это не совсем красивое решение, т.к. приходится сравнивать значения ссылок и, кроме того, это требует, чтобы цикл видел, какой случай произошел.

@ Намного лучший подход должен использовать расширение типа и преобразования представления. Сначала мы расширяем тип Timing_Event, чтобы включать дополнительную информацию о событии (в этом случае цвет) так, чтобы мы могли идентифицировать специфический случай изнутри обработчика:

  1        type Egg_Event is new Timing_Event with
  2                record
  3                        Event_Colour : Colour;
  4                end record;

@ Мы тогда объявляем массив этих расширенных событий (они не должны быть aliased):

  1        Eggs_Done : array (Colour) of Egg_Event;

@ Мы можем теперь вызвать процедуру Set_Handler для яйца в красном держателе:

  1        Set_Handler (Eggs_Done (Red), For_Time, Is_Done'Access);

@ Это - фактически вызов Set_Handler для типа Egg_Event унаследованный от Timing_Event. Но это - тот же самый код так или иначе.

@ Напомним, что значения теговых типов всегда передается по ссылке. Это означает, что изнутри процедуры Is_Done мы можем возвратить основной тип и так обнаружить информацию в расширении. Это сделано при использовании преобразований представления.

@ Фактически мы должны использовать два преобразования представления, сначала мы преобразовываем в наклассовый тип Timing_Event'Class и затем в определенный тип Egg_Event. И затем мы можем выбрать компоненту Event_Colour. Фактически мы можем сделать эти операции в одном операторе таким образом:

  1        procedure Is_Done (E : in out Timing_Event) is
  2                C : constant Colour := Egg_Event (Timing_Event'Class (E)).Event_Colour;
  3        begin
  4                -- egg in holder colour C is ready
  5                ...
  6        end Is_Done;

@ Отметим, что производится проверка при преобразовании из надклассового типа Timing_Event'Class в определенный тип Egg_Event чтобы гарантировать что объект действительно типа Egg_Event (или дальнейшее расширение этого). Если при проверке окажется что это не так возбуждается исключение Tag_Error. Чтобы избежать этой опасности, мы можем использовать тест членства. Например:

  1        procedure Is_Done (E : in out Timing_Event) is
  2                C : Colour;
  3        begin
  4                if Timing_Event'Class (E) in Egg_Event then
  5                        C := Egg_Event (Timing_Event'Class (E)).Event_Colour;
  6                        -- egg in holder colour C is ready
  7                        ...
  8                else
  9                        -- unknown event – not an egg event!
 10                        raise Not_An_Egg;
 11                end if;
 12        end Is_Done;

@ Тест членства гарантирует, что событие имеет соответствующий тип Egg_Event. Мы могли бы избежать двойного преобразования в надклассовый тип введя промежуточную переменную.

@ Важно ценить, что никакая диспетчеризация не вовлечена в эти операции вообще - все является статическим кроме теста членства.

@ Конечно, это было бы немного более гибко, если бы различные подпрограммы брали параметр типа Timing_Event'Class, но это находилось бы в противоречии с Restrictions идентификатором No_Dispatch. Отметим, что сам режим Ravenscar не налагает No_Dispatch, но ограничение находится в приложении Высокой целостности и, таким образом, могло бы быть введено для некоторых приложений высокой целостности, которые могли бы однако желать использовать таймеры простым способом.

@ Несколько незначительных различий между таймерами стоит суммировать.

@ У обоих таймеров центрального процессора есть константа Min_Handler_Ceiling. Это предотвращает нарушение потолка. Это не необходимо для таймера реального времени, потому что запрос защищенной процедуры обрабатывается как прерывание и таким образом в прерывании, перекрывающем уровень.

@ У таймера бюджета группы и таймера реального времени нет исключения, соответствующего Timer_Resource_Error для таймера центрального процессора еденичной задачи. Как упоминалось выше, ожидается, что единственный таймер мог бы быть осуществлен на вершине системы POSIX, когда мог бы быть предел числу таймеров тем более, что каждая задача могла использовать несколько таймеров. В случае группы задача может быть только в одной группе, таким образом число таймеров группы - обязательно меньше, чем число задач и никакого предела, вероятно, не будет превышено. В случае реального времени события просто помещаются в очередь задержки, и никакие другие ресурсы не требуются так или иначе.

@ Нужно также отметить, что таймер группы может использоваться, чтобы контролировать время выполнения единственной задачи. Однако, задача может быть только в одной группе и, таким образом, только один таймер может быть применен к задаче, тогда как (как только было упомянуто), единственный таймер центрального процессора является весьма различным, так как у данной задачи может быть несколько наборов таймеров истекающих в разное время. Таким образом, у обоих видов таймеров есть свои собственные отличные шаблоны использования.

@ ENG RUS

TOP BACK NEXT

2010-10-24 00:26:55

бижутерия из металла . .