Rationale for Ada 2012

John Barnes
Contents   Index   References   Search   Previous   Next 

5.3 Multiprocessors

In recent years the cost of processors has fallen dramatically and for many applications it is now more sensible to use several individual processors rather than one high performance processor.
Moreover, society has got accustomed to the concept that computers keep on getting faster. This makes them applicable to more and more high volume but low quality applications. But this cannot go on. The finite value of the velocity of light means that increase in processor speed can only be achieved by using devices of ever smaller size. But here we run into problems concerning the nonzero size of Planck's constant. When devices get very small, quantum effects cause problems with reliability.
No doubt, in due course, genuine quantum processors will emerge based perhaps on attributes such as spin. But meanwhile, the current approach is to use multiprocessors to gain extra speed.
One special feature of Ada 2012 aimed at helping to use multiprocessors is the concept of synchronous barriers which were described above. We now turn to facilities for generally mapping tasks onto numbers of processors.
The key feature is a new child package of System thus
package System.Multiprocessors is
   pragma Preelaborate(Multiprocessors);
   type CPU_Range is range 0 .. implementation-defined;
   Not_A_Specific_CPU: constant CPU_Range := 0;
   subtype CPU is CPU_Range range 1 .. CPU_Range'Last;
   function Number_Of_CPUs return CPU;
end System.Multiprocessors;
Note that this is a child of System rather than a child of Ada. This is because System is generally used for hardware related features.
Processors are given a unique positive integer value from the subtype CPU. This is a subtype of CPU_Range which also includes zero; zero is reserved to mean not allocated or unknown and for clarity is the value of the constant Not_A_Specific_CPU.
The total number of CPUs is determined by calling the function Number_Of_CPUs. This is a function rather than a constant because there could be several partitions with a different number of CPUs on each partition. And moreover, the compiler might not know the number of CPUs.
Since this is not a Remote Types package, it is not intended to be used across partitions. It follows that a CPU cannot be used by more than one partition. The allocation of CPU numbers to partitions is not defined; each partition could have a set starting at 1, but they might be numbered in some other way.
Tasks can be allocated to processors by an aspect specification. If we write
task My_Task
   with CPU => 10;
then My_Task will be executed by processor number 10. In the case of a task type then all tasks of that type will be executed by the given processor. The expression giving the processor for a task can be dynamic.
Moreover, in the case of a task type, the CPU can be given by a discriminant. So we can have
task type Slave(N: CPU_Range)
   with CPU => N;
and then we can declare
Tom: Slave(1);
Dick: Slave(2);
Harry: Slave(3);
and Tom, Dick and Harry are then assigned CPUs 1, 2 and 3 respectively. We could also have
Fred: Slave(0);
and Fred could then be executed by any CPU since 0 is Not_A_Specific_CPU.
The aspect can also be set by a corresponding pragma CPU. (This is an example of a pragma born obsolescent as explained in Section 2.2.) The aspect CPU can also be given to the main subprogram in which case the expression must be static.
Further facilities are provided by the child package System.Multiprocessors.Dispatching_Domains as shown below. Again we have added use clauses to save space and also have often abbreviated Dispatching_Domain to D_D.
with Ada.Real_Time; with Ada.Task_Identification;
use Ada.Real_Time; use Ada.Task_Identification;
package System.Multiprocessors.Dispatching_Domains is
   pragma Preelaborate(Dispatching_Domains);
   Dispatching_Domain_Error: exception;
   type Dispatching_Domain(<>) is limited private;
   System_Dispatching_Domain: constant D_D;
   function Create(First, Last: CPU) return D_D;
   function Get_First_CPU(Domain: D_D) return CPU;
   function Get_Last_CPU(Domain: D_D) return CPU;
   function Get_Dispatching_Domain(
                         T: Task_Id := Current_Task) return D_D;
   procedure Assign_Task(
                         Domain: in out Dispatching_Domain;
                         CPU: in CPU_Range := Not_A_Specific_CPU;
                         T: in Task_Id := Current_Task);
   procedure Set_CPU(CPU: in CPU_Range;
                         T: in Task_Id := Current_Task);
   function Get_CPU(T: in Task_Id := Current_Task)
                         return CPU_Range;
   procedure Delay_Until_And_Set_CPU(
                         Delay_Until_Time: in Time;
                         CPU: in CPU_Range);
end System.Multiprocessors.Dispatching_Domains;
The idea is that processors are grouped together into dispatching domains. A task may then be allocated to a domain and it will be executed on one of the processors of that domain.
Domains are of the type Dispatching_Domain. This has unknown discriminants and consequently uninitialized objects of the type cannot be declared. But such an object can be initialized by the function Create. So to declare My_Domain covering processors from 10 to 20 inclusive we can write
My_Domain: Dispatching_Domain := Create(10, 20);
All CPUs are initially in the System_Dispatching_Domain. A CPU can only be in one domain. If we attempt to do something silly such as create overlapping domains by for example also writing
My_Domain_2: Dispatching_Domain := Create(20, 30);
then Dispatching_Domain_Error is raised because in this case, CPU number 20 has been assigned to both My_Domain and My_Domain_2.
The environment task is always executed on a CPU in the System_Dispatching_Domain. Clearly we cannot move all the CPUs from the System_Dispatching_Domain other wise the environment task would be left high and dry. Again an attempt to do so would raise Dispatching_Domain_Error.
A very important rule is that Create cannot be called once the main subprogram is called. Moreover, there is no operation to remove a CPU from a domain once the domain has been created. So the general approach is to create all domains during library package elaboration. This then sets a fixed arrangement for the program as a whole and we can then call the main subprogram.
Each partition has its own scheduler and so its own set of CPUs, dispatching domains and so on.
Tasks can be assigned to a domain in two ways. One way is to use an aspect
task My_Task
   with Dispatching_Domain => My_Domain;
If we give both the domain and an explicit CPU thus
task My_Task
   with CPU => 10, Dispatching_Domain => My_Domain;
then they must be consistent. That is the CPU given must be in the domain given. If it is not then task activation fails (hands up all those readers who thought it was going to raise Dispatching_Domain_Error). If for some reason we write
task My_Task
   with CPU => 0, Dispatching_Domain => My_Domain;
then no harm is done. Remember that there is not a CPU with number zero but zero simply indicates Not_A_Specific_CPU. In such a case it would be better to write
task My_Task
   with CPU => Not_A_Specific_CPU, Dispatching_Domain => My_Domain;
The other way to assign a task to a domain is by calling the procedure Assign_Task. Thus the above examples could be written as
Assign_Task(My_Domain, 10, My_Task'Identity);
giving both domain and CPU, and
Assign_Task(My_Domain, T => My_Task'Identity);
which uses the default value Not_A_Specific_CPU for the CPU.
Similarly, we can assign a CPU to a task by
Set_CPU(A_CPU, My_Task'Identity);
Various checks are necessary. If the task has been assigned to a domain there is a check to ensure that the new CPU value is in that domain. If this check fails then Dispatching_Domain_Error is raised. Of course, if the new CPU value is zero, that is Not_A_Specific_CPU then it simply means that the task can then be executed on any CPU in the domain.
To summarize the various possibilities, a task can be assigned a domain and possibly a specific CPU in that domain. If no specific CPU is given then the scheduling algorithm is free to use any CPU in the domain for that task.
If a task is not assigned to a specific domain then it will execute in the domain of its activating task. In the case of a library task the activating task is the environment task and since this executes in the System_Dispatching_Domain, this will be the domain of the library task.
The domain and any specific CPU assigned to a task can be set at any time by calls of Assign_Task and Set_CPU. But note carefully that once a task is assigned to a domain other than the system dispatching domain then it cannot be assigned to a different domain. But the CPU within a domain can be changed at any time; from one specific value to another specific value or maybe to zero indicating no specific CPU.
It is also possible to change CPU but for the change to be delayed. Thus we might write
            Delay_Until_Time => Sometime,
            CPU => A_CPU);
Recall we also have Delay_Until_And_Set_Deadline in Ada.Dispatching.EDF mentioned earlier.
Note that calls of Set_CPU and Assign_Task are defined to be task dispatching points. However, if the task is within a protected operation then the change is deferred until the next task dispatching point for the task concerned. If the task is the current task then the effect is immediate unless it is within a protected operation in which case it is deferred as just mentioned. Finally, if we pointlessly assign a task to the system dispatching domain when it is already in that domain, then nothing happens (it is not a dispatching point).
There are various functions for interrogating the situation regarding domains. Given a domain we can find its range of CPU values by calling the functions Get_First_CPU and Get_Last_CPU. Given a task we can find its domain and CPU by calling Get_Dispatching_Domain and Get_CPU. If a task is not assigned a specific CPU then Get_CPU naturally returns Not_A_Specific_CPU.
In order to accommodate interrupt handling the package Ada.Interrupts is slightly modified and now includes the following function
function Get_CPU(Interrupt: Interrupt_Id)
       return System.Multiprocessors.CPU_Range;
This function returns the CPU on which the handler for the given interrupt is executed. Again the returned value might be Not_A_Specific_CPU.
The Ravenscar profile is now defined to be permissible with multiprocessors. However, there is a restriction that tasks may not change CPU. Accordingly the definition of the profile now includes the following restriction
No_Dependence => System.Multiprocessors.Dispatching_Domains
In order to clarify the use of multiprocessors with group budgets the package Ada.Execution_Time.Group_Budgets introduced in Ada 2005 is slightly modified. The Ada 2005 version is
with 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);}
   ...        -- and so on
end Ada.Execution_Time.Group_Budgets;
However, in Ada 2012 the type Group_Budget has a discriminant giving the CPU thus
type Group_Budget(CPU: System.Multiprocessors.CPU :=
            is tagged limited private;
This means that a group budget only applies to a single processor. If a task in a group is executed on another processor then the budget is not consumed. Note that the default value for CPU is CPU'First which is always 1.
Note that the definition of dispatching domains above assumes that the set of CPU values is contiguous. After ISO standardization it was realised that this was unreasonable and accordingly the definition was changed to allow any set of values as described in Section 9.5 of the Epilogue.

Contents   Index   References   Search   Previous   Next 
© 2011, 2012, 2013 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association:


and   Ada-Europe: