Q:

Questions about implementation of worker tasks controlled by single master task

I would like to implement a master task controlling several instances of a worker task. Each worker task has three different phases:

Initialization
Do work
Report results

At the beginning the master task should initialize all worker tasks (concurrently). Each worker task has then s seconds to successfully complete its initialization but the completion in s seconds is not guaranteed.
What efficient possibilities (signaling mechanisms) do I have to let the master task monitor the state of the initialization of all worker tasks? I thought to give each worker task access to a worker task specific protected type object with a procedure to set a boolean flag which would be set by the individiual worker tasks after they have successfully completed their initialization.
After the master task has triggered the initialization of all worker tasks it could remember the current time and enter a loop to periodically poll the worker tasks initialization states by using a function declared in the protected object type to retrieve the initialization state. The loop is then exited if all worker tasks have been initialized or s seconds have been passed.
Do I have to use such a polling concept using a delay statement inside the monitor loop using an appropriately time value? I read about timeouts of entry calls. Could I use such timeouts to prevent the polling?
After a worker task has been successfuly completed its initialization it should wait for a signal from the control task to execute one work package. So I think a worker task should have a Do_Work entry and the master task therefore should call these entries for all worker tasks in a loop, right?
The master task could use an appropriate mechanism to check if all worker tasks have been completed their work packages. After this has happened the worker tasks should report their work results but in a deterministic way (not concurrently). So if I use a Report_Result entry in the worker tasks to wait for a signal from the master task the call of this entries in a loop in the control task would lead to a non-deterministic order of the report results. Can these entries also be called in blocking way (like a normal procedure call)?

A:

You are correct that the master task can call the Do_Work entry for each worker task.
Similarly, the master task can call the Report_Result entry of all worker tasks. 
A simple way to accomplish this is to create a task type for the worker tasks, and then an array of the worker tasks.
    procedure Master is
       task type Workers is
          entry Do_Work;
          entry Report_Result;
       end Workers;

       Team : array(1..5) of Workers;

    begin
       -- Initialization will occur automatically
       -- Signal workers to Do_Work

       for Worker of Team loop
          Worker.Do_Work;
       end loop;

       -- Create a loop to signal all reports
       -- While the workers may finish in a random order, the 
       -- reporting will occur in the order of the array indices

       for Worker of Team loop
          Worker.Report_Result;
       end loop;
    end Master;

This example is incomplete because it does not define the task body for the Workers task type. The important features of this program are:

Task initialization of the workers in the Team array begins when execution reaches the begin statement in Master.
The Master will wait for each element of Team to accept the entry call to Do_Work.
Each element of Team will wait at the accept statement for Master to call the Do_Work entry.
The master will wait for each element of Team to accept the Report_Result entry.
Each element of Team will wait at its accept for Report_Result for the master to call that entry.

The Ada Rendezvous mechanism neatly coordinates all communication between master and each of the workers.

A:

One thing you can do if you really want the workers to signal the manager that they are done, is pass the Manager's access to the workers and provide an entry for them to call.  You have to decide how the manager and workers interact when that signal happens.
As an example, I had a manager keep an array of Workers and two lists of accesses to those workers (since they are limited types, you have to use access variables).  One list would keep track of all available workers and the other would keep track of the workers currently doing something.  As workers finish up their work, they signal the manager who removed them from the busy list and puts them in the available list.  When the client requests that the manager do more work, it pulls a worker from the available list and places it on the busy list and starts the worker going.  Here is an example compiled in GNAT 7.1.1:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Containers.Bounded_Doubly_Linked_Lists;

procedure Hello is

    package Tasks is

        type Worker;
        type Worker_Access is access all Worker;

        package Lists is new Ada.Containers.Bounded_Doubly_Linked_Lists
            (Element_Type => Worker_Access);

        task type Manager is

            -- Called by client code
            entry Add_Work;
            entry Stop;

            -- Only called by workers to signal they are
            -- finished
            entry Signal(The_Position : in out Lists.Cursor);
        end Manager;

        task type Worker(Boss : not null access Manager) is
            entry Start(The_Position : Lists.Cursor);
        end Worker;

    end Tasks;

    package body Tasks is

        task body Worker is
            Position : Lists.Cursor := Lists.No_Element;
        begin
            loop
                select
                    accept Start(The_Position : Lists.Cursor) do
                        Position := The_Position;
                    end Start;

                    -- Do stuff HERE
                    delay 0.005;

                    -- Finished so signal the manager
                    Boss.Signal(Position);
                    Position := Lists.No_Element;

                or
                    terminate;
                end select;
            end loop;
        end Worker;

        Worker_Count : constant := 10;

        task body Manager is

            -- Worker Pool
            Workers : array(1..Worker_Count) 
                of aliased Worker(Manager'Unchecked_Access);  -- ' Fixing formatting

            -- Use 2 lists to keep track of who can work and who
            -- is already tasked
            Bored : Lists.List(Worker_Count);
            Busy  : Lists.List(Worker_Count);

            -- Gonna call a couple of times, so use a nested
            -- procedure.  This procedure removes a worker 
            -- from the Busy list and places it on the Bored
            -- list.
            procedure Handle_Signal(Position : in out Lists.Cursor) is
            begin
                Put_Line("Worker Completed Work");
                Bored.Append(Lists.Element(Position));
                Busy.Delete(Position);
            end Handle_Signal;

            use type Ada.Containers.Count_Type;

        begin

            -- Start off all workers as Bored
            for W of Workers loop
                Bored.Append(W'Unchecked_Access);  -- ' Fixing formatting
            end loop;

            -- Start working
            loop
                select
                    when Bored.Length > 0 =>
                    accept Add_Work do
                        -- Take a worker from the Bored list, put it
                        -- on the busy list, and send it off to work.
                        -- It will signal when it is finished
                        Put_Line("Starting Worker");
                        Busy.Append(Bored.First_Element);
                        Bored.Delete_First;
                        Busy.Last_Element.Start(Busy.Last);
                    end Add_Work;
                or
                    accept Stop;
                    Put_Line("Received Stop Signal");

                    -- Wait for all workers to finish
                    while Busy.Length > 0 loop
                        accept Signal(The_Position : in out Lists.Cursor) do
                            Handle_Signal(The_Position);
                        end Signal;
                    end loop;

                    -- Break out of loop
                    exit;
                or
                    accept Signal(The_Position: in out Lists.Cursor) do
                        Handle_Signal(The_Position);
                    end Signal;
                end select;
            end loop;

            -- Work finished!
            Put_Line("Manager is Finished");
        end Manager;

    end Tasks;

    Manager : Tasks.Manager;

begin

    for Count in 1 .. 20 loop
        Manager.Add_Work;
    end loop;

    Manager.Stop;

    -- Wait for task to finish
    loop
        exit when Manager'Terminated;
    end loop;

    Put_Line("Program is Done");

end Hello;

I use cursors to help the worker remember where in the busy list they were, so that they can tell the Manager, and it can quickly move things around.
Sample Output:
$gnatmake -o hello *.adb
gcc -c hello.adb
gnatbind -x hello.ali
gnatlink hello.ali -o hello
$hello
Starting Worker
Starting Worker
Starting Worker
Starting Worker
Starting Worker
Starting Worker
Starting Worker
Starting Worker
Starting Worker
Starting Worker
Worker Completed Work
Starting Worker
Worker Completed Work
Starting Worker
Worker Completed Work
Starting Worker
Worker Completed Work
Starting Worker
Worker Completed Work
Starting Worker
Worker Completed Work
Starting Worker
Worker Completed Work
Starting Worker
Worker Completed Work
Worker Completed Work
Starting Worker
Worker Completed Work
Starting Worker
Starting Worker
Received Stop Signal
Worker Completed Work
Worker Completed Work
Worker Completed Work
Worker Completed Work
Worker Completed Work
Worker Completed Work
Worker Completed Work
Worker Completed Work
Worker Completed Work
Worker Completed Work
Manager is Finished
Program is Done

Note that you can pretty this up and hide a bunch of things, I just wanted to get a quick example out.

