Rationale for Ada 2012
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);
private
...
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_And_Set_CPU(
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
private
...
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 :=
System.Multiprocessors.CPU'First)
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.
© 2011, 2012, 2013 John Barnes Informatics.
Sponsored in part by: