THE SELECT STATEMENT ==================== *Consider the following two code fragments ------------------------------------------ select -- S1 select -- S2 accept A; accept A; or else delay 10.0; delay 10.0; end select; end select; If there is an outstanding call on A then behaviour identical If no waiting calls: - S1 will wait for 10 seconds and then timeout, - S2 times out immediately and then stalls for 10 seconds. *Consider the following two code fragments ------------------------------------------ select -- S1 select -- S2 accept A; accept A; or else delay 10.0; delay 20.0 delay 10.0; end select end select; - S1 accepts a call to A any time in first 10 seconds and then delays a further 10 seconds, - S2 will accept any outstanding call to A. If there are no such calls, S2 will pause for 20 seconds. * The following code creates 5 tasks then terminates: ----------------------------------------------------- You need to add a select and terminate option. Note that the last print statement from the task will not, however, be printed. In the given example, the main task does _not_ terminate, since it has child tasks which are yet to finish. with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Terminate_Test is Size : constant Positive := 5; task type Task_Type is entry Start; end Task_Type; task body Task_Type is begin Put_Line ("Beginning of task"); select accept Start do Put_Line ("Start Task "); end Start; or terminate; end select; Put_Line ("End of task"); end Task_Type; Task_Array : array (Positive range 1..Size) of Task_Type; begin null; -- Main task ends here end Terminate_Test; *In lab3 you used accept to implement a code in which the master thread communicated a number to the first or several child threads. ... Generalize this code so that * any of the child processes can receive the initial counter from the master, pass it on and return the final result to the master * the whole process can be repeated arbitrary number of times, i.e. the master can hand out one counter (to any child) get the result back and then hand out another counter (possibly to a different child). * The master can terminate this process by informing each of the child tasks that it wishes to terminate. ---------------------------------------------------------------- with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Ring is type Ring_Range is mod 5; task type Task_Type is entry Receive_Task_Id (Task_Id : in Ring_Range); entry Start ( Start_Val : in Natural ); entry Ring (Count_In : in Natural); entry Stop ( Count_Out : out Natural ); entry Die; end Task_Type; Task_Array : array (Ring_Range) of Task_Type; task body Task_Type is Counter : Natural := 0; To_Whom : Ring_Range; Id : Ring_Range; Alive : Boolean := True; begin accept Receive_Task_Id (Task_Id : in Ring_Range) do Id := Task_Id; end Receive_Task_Id; Put_Line("Hello From Task " & Id'Img ); delay 1.0; To_Whom := Id + 1; while Alive loop select accept Start ( Start_Val : in Natural ) do Counter := Start_Val; end Start; Put_Line("Task " & Id'Img & " starting with token " & Counter'Img ); Task_Array (To_Whom).Ring(Counter); accept Ring (Count_In : Natural) do Counter := Count_In + 1; Put_Line("Task " & Id'Img & " setting token to " & Counter'Img ); end Ring; accept Stop ( Count_Out : out Natural ) do Count_Out := Counter; end Stop; or accept Ring (Count_In : Natural) do Counter := Count_In + 1; Put_Line("Task " & Id'Img & " setting token to " & Counter'Img ); end Ring; Task_Array (To_Whom).Ring(Counter); or accept Die do Alive := False; end Die; end select; end loop; end Task_Type; Global_Counter : Natural := 0; Start : Natural := 0; begin -- start children for I in Task_Array'Range loop Task_Array(I).Receive_Task_Id (I); end loop; -- Now conduct N trials, each starting with a different task. for I in Task_Array'Range loop Task_Array (I).Start( 0 ); Task_Array (I).Stop( Global_Counter ); Put_Line("Master received value " & Global_Counter'Img ); Start := Start + 1; end loop; -- Now inform each child that it should terminate. for I in Task_Array'Range loop Task_Array(I).Die; end loop; end Ring; REQUEUE ======= The following code implements a producer/consumer problem. There is a single producer which is the master task. It produces single integer values from 1...N (N=200) that are to then "consumed" by size consumer tasks (size=5). All each consumer does is read the integer variable and sum it into a local. * The code is of course broken (in a few ways). Your task is to fix it (but do not remove the requeue)! Also define a protected shared natural (take code from last week's lab) and sum the local totals on each child into the shared natural. Have each child print out the value of the shared natural after adding their contribution. The value printed by the last child process to execute these commands should equal the value printed as "Grand Total should be:" on the master. Problem number 1: Each consumer reports a local sum of 0. Cause: Each consumer initialises its local total to 0, and then tries to get data. This results in it being blocked on the 'Data_Available' flag. However, this flag is never set to True. Thus, the timeout in each consumer fires, and the consumer terminates without having added anything to its local total. Fix: Add the line 'Data_Available := True;' to the procedure Put_Data. Recompile and run. You will then discover: ========== Problem number 2: The values that each consumer reports do not sum to the expected total. Cause: The producer is not aware that it is overflowing the (1-element) buffer. Thus, some numbers are being overwritten before they can be incorporated into a local sum. Fix: Forbid the producer from overwriting data. Since the aim of this exercise is to practice using the requeue statement, we'll do that. The code to do this is below. Note that requeue statements cannot appear in procedures. entry Put_Data(Value_Put : in Natural) when True is begin if not Data_Available then The_Data := Value_Put; Data_Available := True; else requeue Wait_For_Put; end if; end Put_Data; entry Wait_For_Put( Value_Put : in Natural ) when not Data_Available is begin The_Data := Value_Put; Data_Available := True; end Wait_For_Put; ========== You will also need to use a Shared_Natural variable to accumulate a running overall total. The final, working version of the code is: with Ada.Text_IO ; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; with Protected_Shared_Natural; use Protected_Shared_Natural; procedure ProducerConsumer_Test is Size : constant Positive := 5; protected type ProducerConsumer is entry Get_Data (Value_Get : out Natural); entry Put_Data (Value_Put : in Natural); private entry Wait_For_Get (Value_Get : out Natural); entry Wait_For_Put (Value_Put : in Natural); Data_Available : Boolean := False; The_Data : Natural; end ProducerConsumer; protected body ProducerConsumer is entry Get_Data (Value_Get : out Natural) when true is begin if Data_Available then Value_Get := The_Data; Data_Available := False; else requeue Wait_For_Get; end if; end Get_Data; entry Wait_For_Get(Value_Get : out Natural) when Data_Available is begin Value_Get := The_Data; Data_Available := False; end Wait_For_Get; entry Put_Data(Value_Put : in Natural) when True is begin if not Data_Available then The_Data := Value_Put; Data_Available := True; else requeue Wait_For_Put; end if; end Put_Data; entry Wait_For_Put( Value_Put : in Natural ) when not Data_Available is begin The_Data := Value_Put; Data_Available := True; end Wait_For_Put; end ProducerConsumer; PassValue : ProducerConsumer; Grand_Total : Shared_Natural ( 0 ); task type Consumer is entry Supply_Id (Id : in Positive); end Consumer; task body Consumer is Consumer_Id, My_Value, My_Total : Natural; Got_Value : Boolean := True; begin accept Supply_Id (Id : in Positive) do Consumer_Id := Id; end Supply_Id; Put_Line("Hello From Consumer " & Consumer_Id'Img ); My_Total := 0; while Got_Value loop select PassValue.Get_Data(My_Value); My_Total := My_Total + My_Value; or -- after 2 seconds and no data we assume it is time to end! delay 2.0; Got_Value := False; end select; -- add some delay to permit task swapping delay 0.1; end loop; Put_Line("Consumer " & Consumer_Id'Img & " Total " & My_Total'Img ); Grand_Total.Increment( My_Total ); Grand_Total.Read( My_Total ); Put_Line("Consumer " & Consumer_Id'Img & " sees Grand Total as " & My_Total'Img ); end Consumer; Consumers : array (Positive range 1..Size) of Consumer; N : Integer := 200; begin Put_Line("Hello From the Producer"); -- start consumers for i in Consumers'Range loop Consumers (i).Supply_Id (i); end loop; Put("Grand Total should be: ");Put(N*(N+1)/2);New_Line; for I in 1..N loop PassValue.Put_Data(I); end loop; end ProducerConsumer_Test; * Rewrite the fixed program but without using requeue. There's really no need to use requeue in this situation - entry guards are perfectly sufficient. The ProducerConsumer type (which is badly named, since what it really represents is a 1-element buffer) becomes: protected type ProducerConsumer is entry Get_Data (Value_Get : out Natural); entry Put_Data (Value_Put : in Natural); private Data_Available : Boolean := False; The_Data : Natural; end ProducerConsumer; protected body ProducerConsumer is entry Get_Data (Value_Get : out Natural) when Data_Available is begin Value_Get := The_Data; Data_Available := False; end Get_Data; entry Put_Data(Value_Put : in Natural) when not Data_Available is begin The_Data := Value_Put; Data_Available := True; end Put_Data; end ProducerConsumer; The remainder of the code is identical, since the details of ProducerConsumer were hidden behind an interface, and that interface has not changed. ENTRY FAMILIES ============== * Compile and run the above multi_cast code. It should work as expected. However, now remove the delay statements from the master task and try again. You should find that the code now hangs. What you have observed is called a race condition. Explain what is happening. First, look at the bodies of the entry calls. The Send entry will only note the arrival of data _if there is someone waiting for it_. The receive entry will block until the arrival of data is noted, and then will dequeue Receive(From)'Count tasks. Remember from the comments in last week's lab solutions that once the Receive(0) entry opens, no new tasks are registered as arriving until the entry closes (or the queue is emptied). So, if we got into a situation whereby only two children had called Receive(0) when the master first called Send, only those two children would be dequeued. Further, the second child would see the Count attribute as 0, and so it would set Arrived(From) to False. This closes off the barrier. When the third child arrives, the barrier is thus closed, and will never be reopened, since there are no more Send calls targetting group 0. The first delay statement ensures that by the time Send is called, every child process has had ample time to enqueue on the entry. Thus, they will all be dequeued. However, if we remove this delay statement, then the rate at which tasks execute and the manner in which they are scheduled is critical. Only if all three children beat the master will multi_cast work. Each child does some blocking IO before calling on the entry point, and so it is unlikely that they will all outpace the master. Any tasks that fail to beat the master will be eternally blocked on their first entry call, and will never get a chance to request their personalised data. This situation is termed a 'race condition' because the result depends on which task executed fastest. Such conditions can lead to hard-to-reproduce bugs. Code which relies on race-conditions in order to execute correctly is very fragile, especially in a multiprocessor (as opposed to uniprocessor multi-tasking) system.