I would like to create an array of tasks in a declare block. The tasks require discriminants which are unique to each task. I am not sure what the best solution is for this but I would prefer that I don’t have to dynamically allocate the task objects.
I have provided an example below which hopefully illustrates what I desire to achieve.
Thanks,
N
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Task_Type_Array is
task type TT (Uppercase : Boolean) is
entry Start (N : Integer);
end TT;
task body TT is
Task_N : Integer;
begin
accept Start (N : Integer) do
Task_N := N;
end Start;
if Uppercase then
Put_Line ("IN TASK T: " & Integer'Image (Task_N));
else
Put_Line ("In task T: " & Integer'Image (Task_N));
end if;
end TT;
type Task_Range is range 1 .. 5;
Inputs : array (Task_Range) of Boolean := (True, False, False, True, True);
-- [1] Doesn't work
My_Tasks_1 : array (Task_Range) of TT (Inputs (I in Task_Range => I));
-- [2] Doesn't work
My_Tasks_2 : array (Task_Range) of TT (Inputs);
begin
Put_Line ("In main");
for I in My_Tasks'Range loop
My_Tasks (I).Start (Natural (I));
end loop;
end Show_Task_Type_Array;
After a quick review of my Barnes book, one way to do this with definite discriminated types is to write out the literal or to use functions returning values of the type, of course, but neither of these are options for a limited type like tasks, now are they? I’m not certain there’s a good way to do this without using access types, since that allows for sidestepping the problem posed by limited types.
The way to do this a bit interesting. I actually asked a similar question to this on comp.lang.ada many many years ago. The trick is to use a combination of a default discriminant and a constructing function. See the example tested on the jdoodle online Ada compiler:
with Ada.Text_IO; use Ada.Text_IO;
procedure jdoodle is
-- Make the discriminant defaulted, so that Ada will allow
-- you to store TT objects of any discriminant together
task type TT (Uppercase : Boolean := False) is
entry Start (N : Integer);
end TT;
task body TT is
Task_N : Integer;
begin
accept Start (N : Integer) do
Task_N := N;
end Start;
if Uppercase then
Put_Line ("IN TASK T: " & Integer'Image (Task_N));
else
Put_Line ("In task T: " & Integer'Image (Task_N));
end if;
end TT;
type Task_Range is range 1 .. 5;
Inputs : array (Task_Range) of Boolean := (True, False, False, True, True);
-- This is required because you cannot assign a task via an aggregate.
-- Using a constructing function bypasses this issue.
function New_Task(Uppercase : Boolean) return TT is
begin
return Result : TT(Uppercase);
end New_Task;
--Does work
My_Tasks_3 : array (Task_Range) of TT :=
(New_Task(True),
New_Task(False),
New_Task(False),
New_Task(True),
New_Task(True));
begin
Put_Line ("In main");
for I in My_Tasks_3'Range loop
My_Tasks_3 (I).Start (Natural (I));
end loop;
end jdoodle;
The reasons for this:
Default discriminant: In general, discriminated record generate logically different types when they have different discriminants. So TT(False) is a completely different type than TT(True). Arrays can only contain groups of the same type. If you give the discriminant a default value however, it tells Ada that the compiler will ensure there is enough memory to hold all variants regardless of the discriminant (GNAT reserves the space needed to create the largest variant, Janus Ada uses dynamic memory allocation instead, etc.). This allows you to store all types TT variants in the same array as long as you don’t specify a discriminant value in the array type definition.
The next challenge is how do you initialize the array components since you cannot provide the discriminant in the array type definition? With normal records, you just assign them using aggregate initialization, but Tasks cannot be created that way. However, you can create a task in a function and pass it out as a result (this will use build in place mechanics. To you create a function that takes the discriminant you want as a parameter, create the specific variant of TT in that funciton and return it. You can then use that function to assign initial values to the array components.
Out of curiosity, this wouldn’t work in Ada 1995 because it didn’t have extended return statements, so is there any way to do it under those constraints?
You could potentially wrap the task types inside a discriminated record instead and do an array of those:
with Ada.Text_IO; use Ada.Text_IO;
procedure jdoodle is
-- Make the discriminant defaulted, so that Ada will allow
-- you to store TT objects of any discriminant together
task type TT (Uppercase : Boolean := False) is
entry Start (N : Integer);
end TT;
task body TT is
Task_N : Integer;
begin
accept Start (N : Integer) do
Task_N := N;
end Start;
if Uppercase then
Put_Line ("IN TASK T: " & Integer'Image (Task_N));
else
Put_Line ("In task T: " & Integer'Image (Task_N));
end if;
end TT;
type TT_Record(Uppercase : Boolean := False) is limited record
Impl : TT(Uppercase);
end record;
type Task_Range is range 1 .. 5;
Inputs : array (Task_Range) of Boolean := (True, False, False, True, True);
-- This is required because you cannot assign a task via an aggregate.
-- Using a constructing function bypasses this issue.
function New_Task(Uppercase : Boolean) return TT is
begin
return Result : TT(Uppercase);
end New_Task;
--Does work
My_Tasks_3 : array (Task_Range) of TT :=
(New_Task(True),
New_Task(False),
New_Task(False),
New_Task(True),
New_Task(True));
My_Tasks_4 : array (Task_Range) of TT_Record :=
((Uppercase => True, others => <>),
(Uppercase => False, others => <>),
(Uppercase => False, others => <>),
(Uppercase => True, others => <>),
(Uppercase => True, others => <>));
begin
Put_Line ("In main");
for I in My_Tasks_3'Range loop
My_Tasks_3 (I).Start (Natural (I));
end loop;
for I in My_Tasks_4'Range loop
My_Tasks_4 (I).Impl.Start (Natural (I));
end loop;
end jdoodle;
I haven’t tried to see if there are any other Ada 95 related issues with that, but that is what I would try.
Unfortunately, the box for type aggregates was also added in Ada 2005.
Regardless, this is interesting. When I first learned about extended return statements, I was still green to the language, and, since I don’t use them anyway, I’d forgotten their use for exactly this task.
I decided to allocate the tasks on the heap like this:
pragma Ada_2022;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Unchecked_Deallocation;
procedure Main is
task type TT (input : Boolean) is
entry Start (N : Integer);
end TT;
task body TT is
Task_N : Integer;
begin
accept Start (N : Integer) do
Task_N := N;
end Start;
if Input then
Put_Line ("In task T: "
& Integer'Image (Task_N));
else
Put_Line ("In TASK T: "
& Integer'Image (Task_N));
end if;
end TT;
type Span is range 1 .. 5;
type TT_Ptr is access TT;
procedure Free is new Ada.Unchecked_Deallocation (TT, TT_Ptr);
Input : array (Span) of Boolean := (True, False, False, True, True);
My_Tasks : array (Span) of TT_Ptr := (for I in Span => new TT (Input (I)));
begin
Put_Line ("In main");
for I in My_Tasks'Range loop
My_Tasks (I).Start (Integer(I));
end loop;
-- Do something important
for T of My_Tasks loop
Free (T);
end loop;
end Main;
I could use entries but I need to pass in access types so the tasks need unique discriminants.
function New_Task (Uppercase : Boolean) return TT is
Result : TT (Uppercase => Uppercase);
begin -- New_Task
return Result;
end New_Task;
Returning limited types has gone through a number of changes through the various Ada versions. In Ada-12 and later, you cannot return a local object; in earlier versions, you could.
Since there are limited aggregates now, I wonder why we can’t have task aggregates and protected aggregates as well ? They would be treated just the same. Records with only discriminants as components.
We can’t be returned as local objects, nor be passed a local access value, and if put in an extended return block, they would activate there for some processing before returning to the calling block (whereas as a variable on the stack as through an allocator). Are there other issues ?
edit: Equality isn’t defined on tasks. But it doesn’t make sense on records containing tasks either.
OK, since I had this issue many, many times, I just give you an advice you can ignore.
You should use access to task, always. There are many factors at play and discriminants is only one of many:
Parameters. Add an entry Start and pass parameters when you create a task.
Discriminants. You usually need an access to the container object which precludes arrays etc.
Task termination. The terminate alternative is almost useless. You can use it in a few very rare cases, but normally you cannot. Terminate does not mix with timed selective accept and you cannot use it entry calls. So you usually need a Stop entry accepted at the end of the task body loop.
Controlled types encapsulating tasks do not fly because Initialize is called when tasks are running and Finalize is called when tasks have been stopped. That makes an access to task is a must. You want to start them from Initialize and stop from Finalize. The language choice is safe and consistent with the model finalization of components, but unusable for all practical purposes [*].
Stopping and deallocating a task. The template is:
Worker.Stop; -- Ask the worker to terminate
while not Worker'Terminated loop -- Check if it did
delay 0.001;
end loop;
Free (Worker); -- Now I can free it
I do not know if the latest Ada standard fixed the issue, but in older Ada you would have a leak if you deallocate a running task.
[*] The problem with task components and task access discriminants to a class-wide container is same as the problem of class-wide constructors in general. I do not go into details, but Initialize/Finalize hack is a class-wide constructor and component task start/stop is a type-specific constructor. You cannot reorder them, so the language choice. Until the language provides both constructors user-defined, the problem will stay.
Yep, but they’re also very useful for the case of “I have some [typically default] value that I’ll return, and some processing to do [which may update the value]” — A good example would be Find:
Type Data_Array is Arrau(Positive range <>) of Data_Element;
Function Find( Value : Data_Element; Vector : Data_Array ) return Natural is
Begin
Return Index : Natural:= Natural'First do
-- Linear search, because it's an example.
For X in Vector'Range loop
if Vector(X) = Value then
Index:= X;
return;
end if;
End loop;
End return;
End Find;
I am using the same solution for freeing access to tasks as Dmitry and Simon, I did a blog post a while ago because as far as I know the issue still exists in GNAT (Freeing task-access objects – Deep Blue Capital ) The blog describes the same solution, but shows why it is useful if people want to understand the context a bit more.
Interesting how, in this scenario, the compiler isn’t able to work out the storage requirements without a default value, i.e. the compiler should know that a discriminant of Boolean would (most likely) require one storage unit (i.e. typically a byte) in it’s target architecture, whether True or False is used when creating a task object (and without a default value one would be forced to provide a value anyway) so I’m wondering what difference does the default value make.
I think the use of a default was just to give a means to tell the compiler to pick a universal size that works for all (for some compilers like Janus, this means they pick the size of a pointer and use the heap to allocate the structure. GNAT chooses to just use the size of the largest variant, similar to how a union would be implemented). In absence of that, they wanted the compiler to pick the appropriate size for the type.
Consider the following:
type Integer_Array is array(Positive range <>) of Integer;
type Vector(Capacity : Natural) is record
Elements : Integer_Array(1..Capacity);
end record;
or
type Thing(Double_Size : Boolean) is record
case Double_Size is
when False => Small : Integer_Array(1..10);
when True => Large : Integer_Array(1..20);
end case;
end record;
In those cases the size can be multiple different things depending on discriminant provided. So the compiler can’t tell the size of that type at compile time. Yes in a simple case it should be easy, but the language allows for much more complex cases so the rules were setup to allow for all cases. There’s definitely an argument to be made that maybe the language rules could provide more flexibility though if the type is simple. It’s definitely something you could bring up at the ARG’s github repo via an issue to see if they would make a change to the language.
Side note: GNAT doesn’t support them fully but there are also coextensions:
type Thing(Coextension : not null access Complex_Type) is record
-- other stuff
end record;
v : Thing(new Complex_Type'( initialization ));
Here the language requires the Coextension item being created here to be created on stack along with v instead of on the heap (as the new might make you think). This is another scenario where the size of the type may not be known at compile time.
I was thinking in order to have the same lifetime, they would need to be allocated in the same way, so if one is allocated on the stack, the other is too, if one is allocated on the heap, the other is too, etc. In my example I was saying v was allocated on the stack, so the coextension would need to be allocated the same way (on the stack) in order to preserve the same lifetime rule. I could be wrong, but that was the thought process. I wasn’t saying all coextensions were on the stack.