Do Not Entry: Bounded Queue of Suspension Objects


In an earlier implementation of the dining philosophers example, we used
suspension objects to keep philosopher tasks waiting for chopsticks.
This allowed us to implement the Chopsticks protected object without
using an entry queue.

Here we extend that idea to the Line_Views package.  Previously, tasks
that call Update would get queued on the Wait entry of a semaphore, to
serialize access to package state and to Text_IO.  Now, in this
alternate implementation, we suspend the calling task using its
suspension object.

Implementation

If the Line_Views resource is already in use (meaning that another task
is in the middle of calling Update), then instead waiting in a protected
object entry queue, the calling task suspends itself on a suspension
object.

We use a special, entry-less semaphore object containing a queue of
(pointers to) suspension objects.  During Update, a pointer to the
task's suspension object is added to the queue.  When the task who owns
the resource is done (has left its critical region), it signals the task
at the front of the queue.

If the task calling Update finds that the resource isn't already in use,
then its suspension object is set to True immediately, and the task
proceeds without any waiting.

The spec of the semaphore looks like this:

   protected type Semaphore_Type (Size : Positive) is

      procedure Wait (SO : in Suspension_Object_Access);

      procedure Signal;

   private

      Queue : Queue_Type (Size);

      In_Use : Boolean := False;

   end Semaphore_Type;


This is a bounded semaphore, and we pass in the maximum number of tasks
as a discriminant.

The In_Use flag indicates whether another thread has already claimed the
resource, and Queue is just a bounded queue of pointers to suspension
objects.

Associated with each task is a suspension object.  In order to use a
resource, the task "registers" its suspension object with the semaphore,
and then immediately following that calls Suspend_Until_True.

The span of a task's critical region looks a little like this:

  Semaphore.Wait (My_Suspension_Object'Access);

  Suspend_Until_True (My_Suspension_Object);

  <critical region>

  Semaphore.Signal;


Note that the name Wait is a bit of a misnomer (it's a protected
procedure, not an entry), because no real waiting occurs until calling
Suspend_Until_True, and only then if the suspension object has the value
False.

Wait is implemented as follows:

      procedure Wait (SO : in Suspension_Object_Access) is
      begin
         if In_Use then
            Add (SO, To => Queue);
            Set_False (SO.all);
         else
            In_Use := True;
            Set_True (SO.all);
         end if;
      end Wait;

If the semaphore is in use, then we set the suspension object to False,
to actually suspend the calling task, and add the suspension object to
the queue.

Otherwise, we set the In_Use flag to indicate that the resource has been
claimed, and set the task's suspension object to True.  When the task
calls Suspend_Until_True, the suspension object will already be True,
and the task enters its critical region right away.

When the task leaves its critical region, it calls Signal:

      procedure Signal is
      begin
         pragma Assert (In_Use);

         if Is_Empty (Queue) then
            In_Use := False;
         else
            Set_True (Get_Front (Queue).all);
            Pop (Queue);
         end if;
      end Signal;

An empty queue means there's no other task waiting to use the resource,
so we just set the In_Use flag is to False, and we're done.  Otherwise,
we resume the waiting task at the front of the queue, and remove that
task from the queue.

In order to use this semaphore, the Line_Views package needs suspension
objects.  Since this package is part of the Philosophers subsystem, we
will reuse the suspension objects already being used for chopstick
management.

We move the suspension object array out of the body of the Philosophers
package, and into the spec of a private Internals package:

  private package Philosophers.Internals is

     type Suspension_Object_Array is
        array (Philosopher_Id) of aliased Suspension_Object;

     Semaphores : Suspension_Object_Array;

  end Philosophers.Internals;


Making the package private hides the package from clients outside the
subsystem.  Only packages (really, their bodies) that are children of
package Philosophers are allowed to with Internals.  In a sense, package
Internals is the "body" of the subsystem.

Now that a suspension object is available, we can implement the Update
operation:

   Number_Of_Philosophers : constant := Philosopher_Id'Last;

   LV_Sema_Size : constant := Number_Of_Philosophers - 1;

   LV_Sema : Semaphores.Bounded.Semaphore_Type (LV_Sema_Size);


   procedure Update
     (Id    : in Philosopher_Id;
      State : in State_Type) is

      SO : Suspension_Object renames Internals.Semaphores (Id);

   begin

      LV_Sema.Wait (SO'Access);
      Suspend_Until_True (SO);

      Do_Update (Id, State);

      LV_Sema.Signal;

   end Update;


The task registers its suspension object with the semaphore, by calling
Wait, and then suspends itself.  When the current task (the owner of the
the resource) is done, it signals the semaphore, and that resumes the
suspended task.

I should have implemented Update using a Controlled object to make sure
Signal gets called, and in a real system I probably would.  I didn't do
it that way here because I was trying to keep the focus on the use of a
suspension object as an alternative to an entry queue.

A note about the size of the semaphore queue.  There are five tasks
total, but if one task is executing, we only need keep a maximum of four
tasks waiting.  That's why the size of the semaphore queue is four
instead of five.

This particular example is similar to the example in section D.10 of the
Ada95 Rationale.  You may find it helpful to peruse that section.

The sources below are in a format suitable for use with gnatchop.

--STX
with Ada.Text_IO;

package Duration_IO is
  new Ada.Text_IO.Fixed_IO (Duration);

with Ada.Synchronous_Task_Control;  use Ada.Synchronous_Task_Control;

private package Philosophers.Internals is

   type Suspension_Object_Array is
      array (Philosopher_Id) of aliased Suspension_Object;

   Semaphores : Suspension_Object_Array;

end Philosophers.Internals;



with Ada.Text_IO;             use Ada.Text_IO;
with Duration_IO;             use Duration_IO;
with Philosophers.Internals;
with Semaphores.Bounded;
with Ada.Synchronous_Task_Control;  use Ada.Synchronous_Task_Control;

package body Philosophers.Line_Views is

   Number_Of_Philosophers : constant := Philosopher_Id'Last;

   LV_Sema_Size : constant := Number_Of_Philosophers - 1;

   LV_Sema : Semaphores.Bounded.Semaphore_Type (LV_Sema_Size);

   Current : Natural := 0;

   Count : Natural := Philosopher_Id'Last;


   procedure Do_Update
     (Id    : in Philosopher_Id;
      State : in State_Type) is

   begin

      if Current = Id then
         Put (", ");
      else
         New_Line;
         Current := Id;
         Put ("Philosopher" & Philosopher_Id'Image (Id) & " ");
      end if;

      Put (State_Type'Image (State));

      if State = Eating
        or State = Thinking
      then

         Put (" (");
         Put (Get_Delay (Id), Fore => 0, Aft => 1, Exp => 0);
         Put (")");

      elsif State = Dying then

         Count := Count - 1;

         if Count = 0 then
            New_Line (2);
            Put_Line ("All the philosophers have terminated.");
         end if;

      end if;

   exception
      when others =>
         Put_Line ("An error occured updating philosopher task" &
                   Integer'Image (Id) &
                   " in state " &
                   State_Type'Image (State));

   end Do_Update;


   procedure Update
     (Id    : in Philosopher_Id;
      State : in State_Type) is

      SO : Suspension_Object renames Internals.Semaphores (Id);

   begin

      LV_Sema.Wait (SO'Access);
      Suspend_Until_True (SO);

      Do_Update (Id, State);

      LV_Sema.Signal;

   end Update;

end Philosophers.Line_Views;
package Philosophers.Line_Views is

   pragma Elaborate_Body;

   procedure Update
     (Id    : in Philosopher_Id;
      State : in State_Type);

end Philosophers.Line_Views;




with Ada.Numerics.Float_Random;    use Ada.Numerics.Float_Random;
with Ada.Synchronous_Task_Control; use Ada.Synchronous_Task_Control;
with Ada.Text_IO;

with Philosophers.Internals;  use Philosophers.Internals;

package body Philosophers is

   Update : Update_Access;


   package Id_Management is

      function Get_Id return Philosopher_Id;

   end;
   use Id_Management;

   package body Id_Management is

      Id : Natural := 0;

      function Get_Id return Philosopher_Id is
      begin
         Id := Id + 1;
         return Id;
      end;

   end Id_Management;



   task type Philosopher_Task_Type (Id : Philosopher_Id := Get_Id);

   type Philosopher_Task_Array is
      array (Philosopher_Id) of Philosopher_Task_Type;

   Philosopher_Tasks : Philosopher_Task_Array;



   type Duration_Array is
      array (Philosopher_Id) of Duration;

   Delay_Times : Duration_Array;


   protected Random_Delay is

      procedure Get (Id : in Philosopher_Id);

      procedure Initialize;

   private

      G : Generator;

   end Random_Delay;



   procedure Start
     (Update : in Update_Access) is
   begin

      Ada.Text_IO.Put_Line
        ("Using suspension objects to wait for " &
         "chopsticks and display device.");

      Philosophers.Update := Update;

      Random_Delay.Initialize;

      for Id in Philosopher_Id loop
         Set_True (Semaphores (Id));
      end loop;

   end Start;


   function Get_Delay (Id : Philosopher_Id) return Duration is
   begin
      return Delay_Times (Id);
   end;


   type Boolean_Array is array (Philosopher_Id) of Boolean;

   protected Chopsticks is

      procedure Pick_Up
       (Id      : in     Philosopher_Id;
        Success :    out Boolean);

      procedure Put_Down
        (Id : in Philosopher_Id);

   private

      Available : Boolean_Array := (others => True);

   end Chopsticks;



   task body Philosopher_Task_Type is

      Delay_Time : Duration renames Delay_Times (Id);

      Semaphore : Suspension_Object renames Semaphores (Id);

      Picked_Up_Chopsticks : Boolean;

   begin

      Suspend_Until_True (Semaphore);

      Update (Id, Breathing);

      for I in 1 .. 5 loop

         loop
            Update (Id, Picking_Up);
            Chopsticks.Pick_Up (Id, Picked_Up_Chopsticks);
            exit when Picked_Up_Chopsticks;
            Update (Id, Suspending);
            Suspend_Until_True (Semaphore);
         end loop;

         Random_Delay.Get (Id);
         Update (Id, Eating);
         delay Delay_Time;
         Update (Id, Done_Eating);
         Chopsticks.Put_Down (Id);

         Random_Delay.Get (Id);
         Update (Id, Thinking);
         delay Delay_Time;
         Update (Id, Done_Thinking);

      end loop;

      Update (Id, Dying);

      delay 60.0; -- hack to work around error in my RTS

   end Philosopher_Task_Type;


   protected body Chopsticks is

      procedure Pick_Up
        (Id      : in     Philosopher_Id;
         Success :    out Boolean) is

         Next : constant Philosopher_Id := Id mod 5 + 1;

      begin

         if Available (Id) and Available (Next) then

            Available (Id) := False;
            Available (Next) := False;

            Success := True;

         else

            Set_False (Semaphores (Id));
            Success := False;

         end if;

      end Pick_Up;


      procedure Put_Down
        (Id : in Philosopher_Id) is

         Prev : constant Philosopher_Id := (Id + 3) mod 5 + 1;
         Next : constant Philosopher_Id := Id mod 5 + 1;

      begin

         Available (Id) := True;
         Available (Next) := True;

         Set_True (Semaphores (Prev));
         Set_True (Semaphores (Next));

      end Put_Down;


   end Chopsticks;


   protected body Random_Delay is

      procedure Get (Id : in Philosopher_Id) is

         F : constant Float := Random (G);
      begin
         Delay_Times (Id) := 10 * Duration (F);
      end;

      procedure Initialize is
      begin
         Reset (G);
      end;

   end Random_Delay;


end Philosophers;

package Philosophers is

   subtype Philosopher_Id is Positive range 1 .. 5;

   type State_Type is
      (Breathing,
       Thinking,
       Done_Thinking,
       Picking_Up,
       Suspending,
       Eating,
       Done_Eating,
       Dying);

   type Update_Access is
      access procedure (Id    : in Philosopher_Id;
                        State : in State_Type);

   procedure Start (Update : in Update_Access);

   function Get_Delay (Id : Philosopher_Id) return Duration;

end Philosophers;

package body Queues.Bounded is

   procedure Add
     (Item : in     Item_Type;
      To   : in out Queue_Type) is
   begin
      pragma Assert (To.Length < To.Size);
      To.Items (To.B) := Item;
      To.B := To.B mod To.Size + 1;
      To.Length := To.Length + 1;
   end Add;


   procedure Pop
     (Queue : in out Queue_Type) is
   begin
      pragma Assert (Queue.Length > 0);
      Queue.F := Queue.F mod Queue.Size + 1;
      Queue.Length := Queue.Length - 1;
   end;


   function Get_Front
     (Queue : Queue_Type) return Item_Type is
   begin
      pragma Assert (Queue.Length > 0);
      return Queue.Items (Queue.F);
   end;


   function Is_Empty
     (Queue : Queue_Type) return Boolean is
   begin
      return Queue.Length = 0;
   end;


   function Is_Full
     (Queue : Queue_Type) return Boolean is
   begin
      return Queue.Length = Queue.Size;
   end;


end Queues.Bounded;




generic
   type Item_Type is private;
package Queues.Bounded is

   type Queue_Type (Size : Positive) is limited private;

   procedure Add
     (Item : in     Item_Type;
      To   : in out Queue_Type);

   procedure Pop
     (Queue : in out Queue_Type);

   function Get_Front
     (Queue : Queue_Type) return Item_Type;

   function Is_Empty
     (Queue : Queue_Type) return Boolean;

   function Is_Full
     (Queue : Queue_Type) return Boolean;

private

   type Item_Array is array (Positive range <>) of Item_Type;

   type Queue_Type (Size : Positive) is
      limited record
         Items  : Item_Array (1 .. Size);
         Length : Natural := 0;
         F, B   : Positive := 1;
      end record;

end Queues.Bounded;




package Queues is

   pragma Pure;

end Queues;
package body Semaphores.Bounded is

   protected body Semaphore_Type is


      procedure Wait (SO : in Suspension_Object_Access) is
      begin
         if In_Use then
            Add (SO, To => Queue);
            Set_False (SO.all);
         else
            In_Use := True;
            Set_True (SO.all);
         end if;
      end Wait;


      procedure Signal is
      begin
         pragma Assert (In_Use);

         if Is_Empty (Queue) then
            In_Use := False;
         else
            Set_True (Get_Front (Queue).all);
            Pop (Queue);
         end if;
      end Signal;


   end Semaphore_Type;

end Semaphores.Bounded;









with Ada.Synchronous_Task_Control; use Ada.Synchronous_Task_Control;
with Queues.Bounded;

pragma Elaborate (Queues.Bounded);

package Semaphores.Bounded is

   pragma Elaborate_Body;

   type Suspension_Object_Access is
      access all Suspension_Object;

   package SO_Queues is
     new Queues.Bounded (Suspension_Object_Access);
   use SO_Queues;


   protected type Semaphore_Type (Size : Positive) is

      procedure Wait (SO : in Suspension_Object_Access);

      procedure Signal;

   private

      Queue : Queue_Type (Size);

      In_Use : Boolean := False;

   end Semaphore_Type;

end Semaphores.Bounded;


package Semaphores is

   pragma Pure;

end Semaphores;
with Philosophers.Line_Views; use Philosophers;

procedure Test_Philosophers is
begin
   Start (Line_Views.Update'Access);
end;

Contributed by: Matthew Heaney
Contributed on: May 24, 1999
License: Public Domain

Back