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

16.2 System Structures: Task Types and Task Objects

An Ada task is an interesting structure. It has aspects of a package, a procedure, and a data structure but is really none of these; it is something different altogether:

Program 16.1 illustrates these aspects of tasks.

Program 16.1
A Task within a Main Program

WITH Ada.Text_IO;
PROCEDURE One_Task IS
------------------------------------------------------------------------
--| Show the declaration of a simple task type and one
--| variable of that type.
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: December 1995                                     
------------------------------------------------------------------------

  -- A task type has a specification
  TASK TYPE SimpleTask (Message: Character);

  -- A task type has a body
  TASK BODY SimpleTask IS

  BEGIN -- SimpleTask
    
    FOR Count IN 1..10 LOOP
      Ada.Text_IO.Put(Item => "Hello from Task " & Message);
      Ada.Text_IO.New_Line;
    END LOOP;

  END SimpleTask;

  Task_A: SimpleTask(Message => 'A');

BEGIN -- One_Task

-- Unlike procedures, tasks are not "called" but are activated
-- automatically.

-- Task_A will start executing as soon as control reaches this
-- point, just after the BEGIN but before any of the main program's
-- statements are executed.

  NULL;

END One_Task;
Sample Run
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A

First note the overall structure of the program. A task type, SimpleTask, is declared with a discriminant, Message. This task specification is followed by a task body in which the message is displayed ten times. Next, Task_A is declared as a task variable, usually called a task object, with a discriminant value of 'A'.

Reaching the main BEGIN of this program, we discover that the program has no executable statements, just a NULL statement to satisfy the rule that a procedure must have at least one statement. Yet the sample run shows the task actually displaying Hello from Task A ten times. The task was never called from the main program, yet it executed anyway.

In fact, the task began its execution just after the main BEGIN was reached. In Ada, this is called "task activation": All tasks declared in a given block are activated just after the BEGIN of that block. Here there is only one task, Task_A.

Multiple Task Objects of the Same Type

Program 16.2 shows the declaration of two task objects, Task_A and Task_B. Further, the task type is modified to allow two discriminants, the message and the number of times the message is to be displayed. A discriminant acts here like a parameter of the task, but it is not a fully general parameter; like a variant-record discriminant, it must be of a discrete--integer or enumeration--type. A string, for example, cannot be used as a task discriminant.

Program 16.2
Two Tasks within a Main Program

WITH Ada.Text_IO;
PROCEDURE Two_Tasks IS
------------------------------------------------------------------------
--| Show the declaration of a simple task type and two
--| variables of that type.
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: December 1995                                     
------------------------------------------------------------------------

  -- A task type has a specification
  TASK TYPE SimpleTask (Message: Character; HowMany: Positive);

  -- A task type has a body
  TASK BODY SimpleTask IS

  BEGIN -- SimpleTask
    
    FOR Count IN 1..HowMany LOOP
      Ada.Text_IO.Put(Item => "Hello from Task " & Message);
      Ada.Text_IO.New_Line;
    END LOOP;

  END SimpleTask;

  -- Now we declare two variables of the type
  Task_A: SimpleTask(Message => 'A', HowMany => 5);
  Task_B: SimpleTask(Message => 'B', HowMany => 7);

BEGIN -- Two_Tasks

-- Task_A and Task_B will both start executing as soon as control 
-- reaches this point, again before any of the main program's 
-- statements are executed. The Ada standard does not specify 
-- which task will start first.

  NULL;

END Two_Tasks;
Sample Run
Hello from Task B
Hello from Task B
Hello from Task B
Hello from Task B
Hello from Task B
Hello from Task B
Hello from Task B
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A
Hello from Task A
As in Program 16.1, Task_A and Task_B are activated just after the main BEGIN. Now there are two tasks; in which order are they activated? The Ada standard does not specify this, leaving it instead up to the compiler implementer. In a short while, we shall see how to control the order in which tasks start their work.

Looking at the sample run from Program 16.2, we see that Task_B evidently started--and completed--its work before Task_A even started its own work. This tells us first that the compiler we used activated Task_B first, and also that, once scheduled for the CPU, Task_B was allowed to continue executing until it completed its run. This seems odd: The tasks do not really execute as though they were running in parallel; there is, apparently, no time-sharing. If there were, we would expect Task_A and Task_B output to be interleaved in some fashion.

In fact, the Ada standard allows, but does not require, time-slicing. Time-slicing, implemented in the run-time support software, supports "parallel" execution by giving each task a slice, usually called a quantum, which is a certain amount of time on the CPU. At the end of the quantum, the run-time system steps in and gives the CPU to another task, allowing it a quantum, and so on, in "round-robin" fashion.

Cooperating Tasks

If Program 16.2 were compiled for a computer with several processors, in theory Task_A and Task_B could have been executed--truly in parallel--on separate CPUs, and no time-slicing would be needed. With a single CPU, we'd like to emulate the parallel operation, ensuring concurrent execution of a set of tasks even if the Ada run-time system does not time-slice.

To get "parallel" behavior portably, using one CPU or many, with or without time-slicing, we code the tasks in a style called cooperative multitasking; that is, we design each task so that it periodically "goes to sleep," giving up its turn on the CPU so that another task can execute for a while.

Program 16.3 shows how this is done, using a DELAY statement in each iteration of the task body's FOR loop. The DELAY causes the task to suspend its execution, or "sleep." Now while Task_A is "sleeping," Task_B can be executing, and so on. The cooperating nature of the two tasks is easily seen in the sample output.

Program 16.3
Using DELAY to Achieve Cooperation

WITH Ada.Text_IO;
PROCEDURE Two_Cooperating_Tasks IS
------------------------------------------------------------------------
--| Show the declaration of a simple task type and two
--| variables of that type. The tasks use DELAYs to cooperate.
--| The DELAY causes another task to get a turn in the CPU.
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: December 1995                                     
------------------------------------------------------------------------

  -- A task type has a specification
  TASK TYPE SimpleTask (Message: Character; HowMany: Positive);

  -- A task type has a body
  TASK BODY SimpleTask IS

  BEGIN -- SimpleTask
    
    FOR Count IN 1..HowMany LOOP
      Ada.Text_IO.Put(Item => "Hello from Task " & Message);
      Ada.Text_IO.New_Line;
      DELAY 0.1;            -- lets another task have the CPU
    END LOOP;

  END SimpleTask;

  -- Now we declare two variables of the type
  Task_A: SimpleTask(Message => 'A', HowMany => 5);
  Task_B: SimpleTask(Message => 'B', HowMany => 7);

BEGIN -- Two_Cooperating_Tasks

-- Task_A and Task_B will both start executing as soon as control 
-- reaches this point, again before any of the main program's 
-- statements are executed. The Ada standard does not specify 
-- which task will start first.

  NULL;

END Two_Cooperating_Tasks;
Sample Run
Hello from Task B
Hello from Task A
Hello from Task B
Hello from Task A
Hello from Task B
Hello from Task A
Hello from Task B
Hello from Task A
Hello from Task B
Hello from Task A
Hello from Task B
Hello from Task B

Controlling the Starting Order of Tasks

We know that the Ada standard does not specify an order of activation for multiple tasks in the same program. Each compiler can use a different order; indeed, a compiler is--theoretically--free to use a different starting order each time the program is run, though practical compilers rarely if ever take advantage of this freedom.

Although we cannot control the actual activation order of tasks, we can gain control of the order in which these tasks start to do their work by using a so-called "start button." This is a special case of a task entry, which is a point in a task at which it can synchronize with other tasks. This is illustrated in Program 16.4.

Program 16.4
Using "Start Buttons" to Control Tasks' Starting Order

WITH Ada.Text_IO;
PROCEDURE Start_Buttons IS
------------------------------------------------------------------------
--| Show the declaration of a simple task type and three
--| variables of that type. The tasks use DELAYs to cooperate.
--| "Start button" entries are used to to control starting order.
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: December 1995                                     
------------------------------------------------------------------------

  TASK TYPE SimpleTask (Message: Character; HowMany: Positive) IS

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

  END SimpleTask;

  TASK BODY SimpleTask IS

  BEGIN -- SimpleTask
    
    -- The task will "block" at the ACCEPT, waiting for the "button"
    -- to be "pushed" (called from another task, Main in this case).
    ACCEPT StartRunning;

    FOR Count IN 1..HowMany LOOP
      Ada.Text_IO.Put(Item => "Hello from Task " & Message);
      Ada.Text_IO.New_Line;
      DELAY 0.1;            -- 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);
  Task_B: SimpleTask(Message => 'B', HowMany => 7);
  Task_C: SimpleTask(Message => 'C', HowMany => 4);

BEGIN -- Start_Buttons

-- Tasks will all start executing as soon as control 
-- reaches this point, but each will block on its ACCEPT
-- until the entry is called. In this way we control the starting
-- order of the tasks.

  Task_B.StartRunning;
  Task_A.StartRunning;
  Task_C.StartRunning;

END Start_Buttons;
Sample Run
Hello from Task B
Hello from Task A
Hello from Task C
Hello from Task B
Hello from Task A
Hello from Task C
Hello from Task B
Hello from Task A
Hello from Task C
Hello from Task B
Hello from Task A
Hello from Task C
Hello from Task B
Hello from Task A
Hello from Task B
Hello from Task B

In this program, the task specification is expanded to include an entry specification:

    ENTRY StartRunning;

This is, syntactically, similar to the subprogram specifications that usually appear in package specifications. The task body includes, immediately after its BEGIN, the corresponding line

    ACCEPT StartRunning;

According to the rules of Ada, a SimpleTask object, upon reaching an ACCEPT statement, must wait at that statement until the corresponding entry is called by another task. In Program 16.4, then, each task--Task_A, Task_B, and Task_C--is activated just after the main program's BEGIN, but--before it starts any work--each reaches its respective ACCEPT and must wait there (in this simple case, possibly forever) until the entry is called.

How is the entry called? In our first three examples, the main program had nothing to do. In this case, its job is to "press the start buttons" of the three tasks, with the entry call statements

    Task_B.StartRunning;
    Task_A.StartRunning;
    Task_C.StartRunning;

These statements are syntactically similar to procedure calls. The first statement "presses the start button" of Task_B. Since Task_B was waiting for the button to be pressed, it accepts the call and proceeds with its work.

The main program is apparently executing--in this case, pressing the start buttons--in "parallel" with the three tasks. In fact, this is true. In a program with multiple tasks, the Ada run-time system treats the main program as a task as well.

SYNTAX DISPLAY
Task Type Specification

Form:
TASK TYPE tname ( optional list of discrimnents ) IS

      ENTRY e1;
      ENTRY e2;
      . . .

    END tname;

Example:
    TASK TYPE Philosopher (Name: Natural) IS

      ENTRY Come_to_Life (First, Second: Chopstick);

    END Philosopher;

Interpretation:
The task type specification gives a list of the entries to be provided by the task objects.

SYNTAX DISPLAY
Task Body

Form:
    TASK BODY tname IS
    	local declaration-section
    BEGIN
    	statement sequence
    END tname;

Example:
The SimpleTask bodies shown in this section serve as examples; we need not repeat them here.

Interpretation:
The task body contains the local declarations and statement sequence of the task. Multiple task objects of the type can be declared; each task object has, in effect, a copy of the task body.

A task body can contain code that is much more interesting than we have seen. Ada provides the SELECT statement to give a programmer much flexibility in coding task bodies. For example, using the SELECT,

The SELECT construct is one of the most interesting in all of programming; entire books can be written about the possibilities it offers. Space does not permit a full discussion of the SELECT here; we hope this brief discussion has sparked your curiosity to learn more about it.

In this section, we have seen the basics of task types and objects. We now proceed to introduce protected types and objects.


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

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