Previous | Next | Table of Contents | Index | Program List | Copyright

16.3 System Structures: Protected Types and Protected Objects

In Section 16.1 we discussed mutual exclusion using the example of an e-mail reader. Here we look at an analogous, but simpler, situation. Suppose we have a three-task program like Program 16.4, but we want each task to write its output in its own area of the screen. The desired output is

    Hello from Task A        Hello from Task B        Hello from Task C
    Hello from Task A        Hello from Task B        Hello from Task C
    Hello from Task A        Hello from Task B        Hello from Task C
    Hello from Task A        Hello from Task B        Hello from Task C
    Hello from Task A        Hello from Task B
                             Hello from Task B
                             Hello from Task B
This simple example is representative of multiwindow programs. We modify the task specification to read
    TASK TYPE SimpleTask (Message: Character; 
                          HowMany: Screen.Depth;
                          Column:  Screen.Width) IS . . .
adding a third discriminant, Column, to indicate which screen column each task should use for the first character of its repeated message. Further, we modify the main loop of the task body as follows:
    FOR Count IN 1..HowMany LOOP
      Screen.MoveCursor(Row => Count, Column => Column);
      Ada.Text_IO.Put(Item => "Hello from Task " & Message);
      DELAY 0.5;            -- lets another task have the CPU
    END LOOP;
That is, the task positions the screen cursor to the proper column before writing the message. Program 16.5 shows the full program.

Program 16.5
Several Tasks Using the Screen

WITH Ada.Text_IO;
WITH Screen;
PROCEDURE Columns IS
------------------------------------------------------------------------
--| Shows tasks writing into their respective columns on the
--| screen. This will not always work correctly, because if the
--| tasks are time-sliced, one task may lose the CPU before 
--| sending its entire "message" to the screen. This may result
--| in strange "garbage" on the screen.
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: December 1995                                     
------------------------------------------------------------------------

  TASK TYPE SimpleTask (Message: Character; 
                        HowMany: Screen.Depth;
                        Column:  Screen.Width) IS

    -- This specification provides a "start button" entry.
    ENTRY StartRunning;

  END SimpleTask;

  TASK BODY SimpleTask IS

  BEGIN -- SimpleTask
    
    -- Each task will write its message in its own column
    ACCEPT StartRunning;

    FOR Count IN 1..HowMany LOOP
      Screen.MoveCursor(Row => Count, Column => Column);
      Ada.Text_IO.Put(Item => "Hello from Task " & Message);
      DELAY 0.5;            -- lets another task have the CPU
    END LOOP;

  END SimpleTask;

  -- Now we declare three variables of the type
  Task_A: SimpleTask(Message => 'A', HowMany => 5, Column => 1);
  Task_B: SimpleTask(Message => 'B', HowMany => 7, Column => 26);
  Task_C: SimpleTask(Message => 'C', HowMany => 4, Column => 51);

BEGIN -- Columns
  
  Screen.ClearScreen;
  Task_B.StartRunning;
  Task_A.StartRunning;
  Task_C.StartRunning;

END Columns;
Sample Run
Hello from Task A        Hello from Task B        Hello from Task C
                                                  2Hello from Task C;26f[2;1fHello from Task AHello from Task B                   [[3;1fHello from Task A3;26fHello from Task BHello from Task C4;4;1fHello from Task A51fHello from Task C26fHello from Task B5;526;f1fHello from Task BHello from Task A
                         Hello from Task B
                         Hello from Task B

The output from running this program is not exactly what we intended! Instead of the desired neat columns, we got messages displayed in seemingly random locations, interspersed with apparent "garbage" like

    C;26f[2;1f
What happened here? To understand this, recall the body of Screen.MoveCursor (included in Program 3.9):
    PROCEDURE MoveCursor (Column : Width; Row : Depth) IS
    BEGIN
      Ada.Text_IO.Put (Item => ASCII.ESC);
      Ada.Text_IO.Put ("[");
      Ada.Integer_Text_IO.Put (Item => Row, Width => 1);
      Ada.Text_IO.Put (Item => ';');
      Ada.Integer_Text_IO.Put (Item => Column, Width => 1);
      Ada.Text_IO.Put (Item => 'f');
    END MoveCursor;

Positioning the cursor requires an instruction, up to eight characters in length, to the ANSI terminal software: the ESC character, then '[', followed by a possibly two-digit Row, then ';', then a possibly two-digit Column value, and finally 'F'. Once it receives the entire instruction, the terminal actually moves the cursor on the screen.

Suppose the MoveCursor call is issued from within a task, as in the present example. Suppose further that in this case the Ada run-time system does provide time-slicing to give "parallel" behavior of multiple tasks. It is quite possible that the task's quantum will expire after only some of the eight characters have been sent to the terminal, and then another task will attempt to write something to the terminal. The terminal never recognized the first instruction, because it received only part of it, so instead of obeying the instruction, it just displays the characters. The "garbage" string above, C;26f[2;1f, consists of pieces from several different intended instructions.

This problem arose because a task was interrupted in mid-instruction, and then another task was allowed to begin its own screen instruction. This is called a race condition because two tasks are, effectively, in a race to write to the screen, with unpredictable results. It is actually a readers-writers problem: Multiple tasks are interfering with each other's attempts to write to the screen.

To prevent this problem from ruining our columnar output, we must protect the screen so that--whether or not we have time-slicing--a task is allowed to finish an entire display operation before another task can begin one. We do this in Ada with a protected type, as shown in Program 16.6.

Program 16.6
Using a Protected Type to Ensure Completion of a Screen Action

WITH Ada.Text_IO;
WITH Screen;
PROCEDURE Protect_Screen IS
------------------------------------------------------------------------
--| Shows tasks writing into their respective columns on the
--| screen. This time we use a protected type, whose procedure
--| can be executed by only one task at a time.
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: December 1995                                     
------------------------------------------------------------------------

  PROTECTED TYPE ScreenManagerType IS

  -- If multiple calls of Write are made simultaneously, each is 
  -- executed in its entirety before the next is begun.
  -- The Ada standard does not specify an order of execution.

    PROCEDURE Write (Item:   IN String; 
                     Row:    IN Screen.Depth; 
                     Column: IN Screen.Width);

  END ScreenManagerType;

  PROTECTED BODY ScreenManagerType IS

    PROCEDURE Write (Item:   IN String; 
                     Row:    IN Screen.Depth; 
                     Column: IN Screen.Width) IS
    BEGIN -- Write

      Screen.MoveCursor(Row => Row, Column => Column);
      Ada.Text_IO.Put(Item => Item);

    END Write;

  END ScreenManagerType;

  Manager: ScreenManagerType;

  TASK TYPE SimpleTask (Message: Character; 
                        HowMany: Screen.Depth;
                        Column:  Screen.Width) IS

    -- This specification provides a "start button" entry.
    ENTRY StartRunning;

  END SimpleTask;

  TASK BODY SimpleTask IS

  BEGIN -- SimpleTask
    
    -- Each task will write its message in its own column
    -- Now the task locks the screen before moving the cursor,
    -- unlocking it when writing is completed.

    ACCEPT StartRunning;

    FOR Count IN 1..HowMany LOOP

      -- No need to lock the screen explicitly; just call the
      -- protected procedure.
      Manager.Write(Row => Count, Column => Column,
                    Item => "Hello from Task " & Message);

      DELAY 0.5;            -- lets another task have the CPU
    END LOOP;

  END SimpleTask;

  -- Now we declare three variables of the type
  Task_A: SimpleTask(Message => 'A', HowMany => 5, Column => 1);
  Task_B: SimpleTask(Message => 'B', HowMany => 7, Column => 26);
  Task_C: SimpleTask(Message => 'C', HowMany => 4, Column => 51);

BEGIN -- Protect_Screen
  
  Screen.ClearScreen;
  Task_B.StartRunning;
  Task_A.StartRunning;
  Task_C.StartRunning;

END Protect_Screen;

In this program, we declare a type

    PROTECTED TYPE ScreenManagerType IS
    
      PROCEDURE Write (Item:   IN String; 
                       Row:    IN Screen.Depth; 
                       Column: IN Screen.Width);
    
    END ScreenManagerType;
    Manager: ScreenManagerType;
An object of this type in this case Manager, provides a procedure Write to which are passed all the parameters of the desired screen operation: the string to be written, the row, and the column. Any task wishing to write to the screen must do so by passing these parameters to the screen manager. The SimpleTask body now contains a call
    Manager.Write(Row => Count, Column => Column,
                  Item => "Hello from Task " & Message);
as required. The body of the protected type is
    PROTECTED BODY ScreenManagerType IS
    
      PROCEDURE Write (Item:   IN String; 
                       Row:    IN Screen.Depth; 
                       Column: IN Screen.Width) IS
      BEGIN -- Write
    
        Screen.MoveCursor(Row => Row, Column => Column);
        Ada.Text_IO.Put(Item => Item);
    
      END Write;
    
    END ScreenManagerType;
and the Write procedure encapsulates the MoveCursor and Put operations.

What is the difference between this protected write procedure and an ordinary procedure? Ada guarantees that each call of a protected procedure will complete before another call can be started. Even if several tasks are running, trading control of the CPU among them, a task will not be allowed to start a protected procedure call if another call of the same procedure, or any other procedure of the same protected object, is still incomplete. In our case, this provides the necessary mutual exclusion for the screen.

Protected types can provide functions and entries in addition to procedures. Protected functions allow for multiple tasks to examine a data structure simultaneously but not to modify the data structure. Protected entries have some of the properties of both task entries and protected procedures. A detailed discussion of these is beyond our scope here.

The next section will introduce a more interesting use of protected types.


Previous | Next | Table of Contents | Index | Program List | Copyright

Copyright © 1996 by Addison-Wesley Publishing Company, Inc.