Dynamic task creation

Hello!

I would like to implement the classic multi-threaded echo-server example in Ada 2012. I found some implementations, but they use a hard-wired fixed number of tasks. I would like to implement it in such a way that it works with any number of clients. I want to dynamically create and terminate tasks. What is the idiomatic Ada method for this? I don’t want to write a C program in Ada language… :slight_smile:
I would solve it with a pointer array (access type), but I read everywhere that the use of pointers should be avoided.
I naively tried to use Vector since containers automatically free memory space occupied by deleted items; however, task is a limited type so it cannot be used in containers.
It also shows that I am quite a beginner :slight_smile:
Could you give me some guidance?

T := new Some_Task;

Thanks, but I know how to dynamically create a task. (Maybe the title of the topic is misleading… :slight_smile: )
My problem is more how to store them: in arrays with pointers or there is some more Ada-compliant method.
For example, should I create a class whose data member is a task? Can I store these in a container (Vector)? How should the storage be freed up after the task has run?
I know these seem like very stupid questions. I am a C/C++ programmer and for this reason I try to find the usual schemes in Ada. But Ada is so different…

You can do it anyway you want, but if you want dynamic creation and destruction, you need to use access types.

That’s what I came up with. Thanks for confirming!

you need to create your own container or data structure that creates limited objects in situ. Like a list whose item is an access to the task. Try this

1 Like

One option is to use a ref counted access type (similar to shared_ptr). You can then just do a vector of those. Doing vectors of limited types directly is really tough since limited types cannot be copied, and vectors require copying to work (not impossible, but tough to do out of the box).

I have a ref counted access type that supports limited types here: bullfrog/src/bullfrog-access_types-smart_access.ads at 0420184ead40fe6ce418d14d738887d9fc779807 · jere-software/bullfrog · GitHub

you can then do something like:

package Task_Holders is new Bullfrog.Access_Types.Smart_Access(My_Task_Type);
subtype Shared_Access is Task_Holders.Shared_Access;
use type Shared_Access; -- to get operators visible

package Task_Vectors is new Ada.Containers.Vectors(Positive, Shared_Access);
subtype Task_Vector is Task_Vectors.Vector;

Tasks : Task_Vector;

and later:

Tasks.Append(Task_Holders.Make.Shared_Access(new My_Task_Type);

and access with:

-- First task in the list's entry.  Reference is an operation
-- from Smart_Access that dereferences the Shared_Access object into your task.
-- It's a bit different from Shared_Ptr, but Ada has different constraints to do this 
-- kind of thing.
Task(1).Reference.Some_Task_Entry;   

NOTE: all of that is hand typed out, so may contain syntax errors. Did it quickly.

You can roll your own ref counted access types as well.

This can be tricky. You’ll either have to force the task to end early using a protected object (or entry, depends on what works for your task design), and then you want to wait until the task is terminated all before you deallocate it. You can try wrapping your task in a limited_controlled type and have you overrriding finalize procedure handle all of that. Even then, I’m no expert here, but there may be more to it when deallocating a task. You can also for terminate a task before deallocating it, but that is really risky depending on the task.

Finalization intro: Ada95 Lovelace Tutorial Section 7.7 - User-Controlled Initialization, Finalization, and Assignment
Finalization package spec: gnatprove_14.1.1_f6ca6f8c/libexec/spark/lib/gcc/x86_64-pc-linux-gnu/14.1.0/adainclude/a-finali.ads - Ada Code Search

Side note, where you declare your container also can have impacts. Task and object finalization can be impacted differently based on if it is declared at library level or in you main procedure. I always try to declare task related stuff at library level when I can.

1 Like

That’s an area of Ada where I’m still a bit vague (I don’t generally use limited types, if I ever have), and it surprises me that a vector requires copying to work. It seems to me that you should only need move semantics to make it work, as (I think) Rust does. Have I misunderstood something? (in either language)

Idiomatically: either a collection of access to a task type, or an array of task types or accesses thereunto.

Also, remember that you can use a define-block and it won’t exit until all tasks have terminated.

Procedure Server( Instances : Natural ) is
   Count : Natural renames Prompt( "How many servers do you wish to start?" );
   Instances : Array (1..Count) of Server_Type:= (Others => <>);
Begin
   null; -- whatever else you need to do.
End Server;

Thank you!

Use of pointers should be avoided; you’re on the right path, though: the way I frame it to people coming from C or C++ is this: Never use pointers. precisely because those languages instill a pointers-for-everything reflex, and in Ada 80-95% of what you’re using pointers for can be done w/o pointers; e.g.: functions as parameters, done using generics; arrays of varying length, Ada’s arrays “know their own length”; varying-length data-components, done with discriminated records, etc.

Someone already posted an link to a discussion that regarding the limited constraint — read that whole thread — I thing you should be able to use a task interface, and instantiate the containers on the 'Class there.

1 Like

Vectors in the Ada standard require copying as defined in the RM. You can definitely roll your own vector type that works with move semantics only, but you also need to ensure that the vector itself isn’t copyable (since you can only move elements and not copy them in that scenario).

Side note, the option I presented of using a reference counted type to the tasks that are stored in vectors, psuedo emulates using a little bit of move semantics (it isn’t fully accurate to say that, but the reference counted types don’t make copies of the task, they only make copies of the reference to the tasks, so it can act a little similar to moves).

2 Likes

Thank you very much for all the comments! I received really valuable help from all of you!

I like to say that access (to object) types are never needed in Ada, but that’s accompanied by a disclaimer that it’s true as a first-order approximation. There are times when access types are needed, and dealing with tasks is a big part of them. But a lot depends on what you’re going to do with the task.

In modern Ada, tasks communicate through calls to protected objects, not through rendezvous, so there’s rarely a need to reference a task once it’s been created. In such cases, you create the task using an access type, but you don’t keep the access value. The task either learns when to end through a call to a protected operation, or it runs forever. See the PragmARC Job Pools for an example.

1 Like