6.1 Tasks, accept Statements and entry Calls

[ Table of Contents ] Chapter Overview ] Next ] [ Glossary/Index ]

A task is a program unit that runs concurrently with other program units and block statements. A task is a nested unit -- never a library unit. Every task has two separate parts: a declaration and a body -- both nested in another unit. A task declaration always contains the reserved words: task, is and end. A task body always contains the reserved words: task, body, is, begin and end. A task type declaration always contains the reserved words: task, type, is, begin and end.

The two forms of a task unit are shown below. When the first form is used, a one-of-a-kind task of an anonymous type is defined. When the second form is used, one can subsequently declare task objects of the named task type.

Expanded task Symbol

General Form of a task

Im6-2a.gif (2521 bytes)

task My_Task is
  entry E1(formal_parameters);
  entry E2(formal_parameters);
  -- etc
end My_Task;
------------------------------
task body My_Task is
  -- hidden declarations
begin
  -- executable part
exception   -- (optional)
  -- handler (optional)
end My_Task;

Expanded task type Symbol

General Form of a task type

Im6-2a1.gif (2639 bytes)

task type My_Task_Type is
  entry E1(formal_parameters);
  entry E2(formal_parameters);
  -- etc
end My_Task_Type;
------------------------------
task body My_Task_Type is
  -- hidden declarations
begin
  -- executable part
exception   -- (optional)
  -- handler (optional)
end My_Task_Type;

The only items that a task can export are entries, pragmas, and representation clauses. In contrast, a package can export many things (such as program units, types, and objects), but cannot export entries. A task declaration may also have a private part where additional "internal" entries are declared. These internal entries may be called only by "local tasks" -- such as tasks nested in the body of this task.

Entry Calls and Accept Statements

A task is called when another unit issues an entry call statement naming the task and one of its entries. A task is not required to have entries. It may simply start up and run automatically when its enclosing unit is elaborated. If a task has one or more entries, the executable part of its body must contain at least one accept statement for every entry. The role of an accept statement is to accept calls to the entry of the same name, as shown in the following example program.

Example Program with Tasks

Both tasks in this example program have a single entry (named Go) and a single accept statement (also named Go). The entry in Task_A is called only once by the enclosing procedure. The entry in Task_B is called multiple times by Task_A because both the accept in Task_B and the entry call in Task_A are inside loops.

Im6-2b.gif (3740 bytes)

Statement Forms

The formal parameters of an accept statement mimic those of the corresponding entry declaration, and the actual parameters of an entry call must match the types and constraints of the entry's formal parameters. The accept statement has two forms, as shown below.

Entry Call
Task_Name.Entry_Name(actual_parameters);
Simple Accept
accept E1(formal_parameters);
Compound Accept
accept E2(formal_parameters) do
  -- statement sequence
end E2;

Source Code Listing

----------------------------------------------------------
--  This three-task program includes the main procedure, 
--  Tester, as the "main task" and two tasks, Task_A and 
--  Task_B, nested inside it. 
----------------------------------------------------------
with Ada.Text_IO, Ada.Calendar;
use  Ada.Text_IO, Ada.Calendar;
procedure Tester is
  Start_Time   : constant Time := Clock;
  Elapsed_Time : Duration;
  
  task Task_A is
    entry Go;
  end Task_A;
  
  task Task_B is
    entry Go;
  end Task_B;
  
  task body Task_A is
  begin
    delay 5.0;                           -- relative delay
    accept Go;
    Elapsed_Time := Clock - Start_Time;
    Put_Line("Tester/Task_A Rendezvous occurred at T = " 
             & Integer'Image(Integer(Elapsed_Time)));
    for I in 1..5 loop
      delay 5.0;                         -- relative delay
      Task_B.Go;
    end loop;  
  end Task_A;
  
  task body Task_B is
  begin
    for I in 1..5 loop
      delay until (Start_Time + I*7.0);  --absolute delay 
      accept Go;
      Elapsed_Time := Clock - Start_Time;
      Put_Line("Task_A/Task_B Rendezvous occurred at T = " 
               & Integer'Image(Integer(Elapsed_Time)));
    end loop;  
  end Task_B;
begin                          
  Task_A.Go;
end Tester;
----------------------------------------------------------

The above program produces the following output:

     Tester/Task_A Rendezvous occurred at T = 5
     Task_A/Task_B Rendezvous occurred at T = 10
     Task_A/Task_B Rendezvous occurred at T = 15
     Task_A/Task_B Rendezvous occurred at T = 21
     Task_A/Task_B Rendezvous occurred at T = 28
     Task_A/Task_B Rendezvous occurred at T = 35

Rendezvous

When the rendezvous occurs between Tester and Task_A, Tester is the "caller" and Task_A is the "callee." When each rendezvous occurs between Task_A and Task_B, Task_A is the caller and Task_B is the callee. The rendezvous mechanism is used to synchronize two tasks (as in this case) and to transfer data between tasks (in cases where parameters are involved). In the first two cases of a Task_A/Task_B rendezvous (at T=10 and T=15), Task_B must wait for Task_A. That is, Task_B reaches its accept statement before Task_A issues its entry call. In the final three cases (at T=21, 28, and 35), Task_A must wait for Task_B because the absolute delays built into Task_B become the pacing items rather than the relative delays built into Task_A.

The select statement (see next section) can be used to control how long the caller and/or the callee will wait for the rendezvous to take place.

Entry Queues

Whenever a calling task issues an entry call to another task, the run-time system places the calling task on a queue associated with that entry. If there are multiple calling tasks, they can be "queued up" in a first-come/first-served manner. (See later example with requeue statement.)

Attributes of Entries and Tasks

Each entry, E, has an attribute E'Count, indicating the number of calling tasks currently on the queue. Each task has attributes T'Callable and T'Terminated, with Boolean results indicating whether the task is currently callable or has already terminated, respectively. (The Count attribute is illustrated in a later requeue statement example.)

Entry Families

An entry family is a set of entries in a task, all having the same name, indexed by an object of a discrete type. An example declaration and entry call follow:

Entry Family Declaration

entry My_Family(1..4);

Indexed Entry Call

My_Task.My_Family(2);

Task Priorities

The urgency of a task can be specified by pragma Priority, such that a task with a higher priority will execute (if possible) while tasks with lower priorities must wait.

Dynamically Created Tasks

Since tasks are objects, they may be created dynamically during execution, using allocators and access variables. Given a task type, TT, we could also declare an access type, "type TT_Ptr is access TT" and then create new tasks as shown below.

  T1 := new TT_Ptr;
  T2 := new TT_Ptr;

Two new (un-named) tasks are thus created. T1 and T2 are access values designating the two tasks.

Related Topics:

2.2  Program Units

[ Back to top of pageNext ]