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 BThis 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
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;1fWhat 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
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.
Copyright © 1996 by Addison-Wesley Publishing Company, Inc.