The Ada Joint Program Office does not guarantee the accuracy of this file, as compared with the contents of ANSI/MIL-STD-1815A-1983, the Reference Manual for the Ada Programming Language. If errors or discrepancies are found in this machine-readable version, please forward comments via the Defense Data Network (DDN) to: ACTION@AJPO.SEI.CMU.EDU or via conventional mail to Ada Information Clearinghouse 3D139 (1211 S. Fern, C-107) The Pentagon Washington, D.C. 20301-3081 ----------------------------------------------------------------------- Copyright 1980, 1982, 1983 owned by the United States Government as represented by the Under Secretary of Defense, Research and Engineering. All rights reserved. Provided that notice of copyright is included on the first page, this document may be copied in its entirety without alteration or as altered by (1) adding text that is clearly marked as an insertion; (2) shading or highlighting existing text; (3) deleting examples. Permission to publish other excerpts should be obtained from the Ada Joint Program Office, OUSDRE (R&AT), The Pentagon, Washington, DC 20301-2081, U.S.A. 9. Tasks The execution of a program that does not contain a task is defined in terms of a sequential execution of its actions, according to the rules described in other chapters of this manual. These actions can be considered to be executed by a single logical processor. Tasks are entities whose executions proceed in parallel in the following sense. Each task can be considered to be executed by a logical processor of its own. Different tasks (different logical processors) proceed independently, except at points where they synchronize. Some tasks have entries. An entry of a task can be called by other tasks. A task accepts a call of one of its entries by executing an accept statement for the entry. Synchronization is achieved by rendezvous between a task issuing an entry call and a task accepting the call. Some entries have parameters; entry calls and accept statements for such entries are the principal means of communicating values between tasks. The properties of each task are defined by a corresponding task unit which consists of a task specification and a task body. Task units are one of the four forms of program unit of which programs can be composed. The other forms are subprograms, packages and generic units. The properties of task units, tasks, and entries, and the statements that affect the interaction between tasks (that is, entry call statements, accept statements, delay statements, select statements, and abort statements) are described in this chapter. Note: Parallel tasks (parallel logical processors) may be implemented on multicomputers, multiprocessors, or with interleaved execution on a single physical processor. On the other hand, whenever an implementation can detect that the same effect can be guaranteed if parts of the actions of a given task are executed by different physical processors acting in parallel, it may choose to execute them in this way; in such a case, several physical processors implement a single logical processor. References: abort statement 9.10, accept statement 9.5, delay statement 9.6, entry 9.5, entry call statement 9.5, generic unit 12, package 7, parameter in an entry call 9.5, program unit 6, rendezvous 9.5, select statement 9.7, subprogram 6, task body 9.1, task specification 9.1 9.1 Task Specifications and Task Bodies A task unit consists of a task specification and a task body. A task specification that starts with the reserved words task type declares a task type. The value of an object of a task type designates a task having the entries, if any, that are declared in the task specification; these entries are also called entries of this object. The execution of the task is defined by the corresponding task body. A task specification without the reserved word type defines a single task. A task declaration with this form of specification is equivalent to the declaration of an anonymous task type immediately followed by the declaration of an object of the task type, and the task unit identifier names the object. In the remainder of this chapter, explanations are given in terms of task type declarations; the corresponding explanations for single task declarations follow from the stated equivalence. task_declaration ::= task_specification; task_specification ::= task [type] identifier [is {entry_declaration} {representation_clause} end [task_simple_name]] task_body ::= task body task_simple_name is [declarative_part] begin sequence_of_statements [exception exception_handler {exception_handler}] end [task_simple_name]; The simple name at the start of a task body must repeat the task unit identifier. Similarly if a simple name appears at the end of the task specification or body, it must repeat the task unit identifier. Within a task body, the name of the corresponding task unit can also be used to refer to the task object that designates the task currently executing the body; furthermore, the use of this name as a type mark is not allowed within the task unit itself. For the elaboration of a task specification, entry declarations and representation clauses, if any, are elaborated in the order given. Such representation clauses only apply to the entries declared in the task specification (see 13.5). The elaboration of a task body has no other effect than to establish that the body can from then on be used for the execution of tasks designated by objects of the corresponding task type. The execution of a task body is invoked by the activation of a task object of the corresponding type (see 9.3). The optional exception handlers at the end of a task body handle exceptions raised during the execution of the sequence of statements of the task body (see 11.4). Examples of specifications of task types: task type RESOURCE is entry SEIZE; entry RELEASE; end RESOURCE; task type KEYBOARD_DRIVER is entry READ (C : out CHARACTER); entry WRITE(C : in CHARACTER); end KEYBOARD_DRIVER; Examples of specifications of single tasks: task PRODUCER_CONSUMER is entry READ (V : out ITEM); entry WRITE(E : in ITEM); end; task CONTROLLER is entry REQUEST(LEVEL)(D : ITEM); -- a family of entries end CONTROLLER; task USER; -- has no entries Example of task specification and corresponding body: task PROTECTED_ARRAY is -- INDEX and ITEM are global types entry READ (N : in INDEX; V : out ITEM); entry WRITE(N : in INDEX; E : in ITEM); end; task body PROTECTED_ARRAY is TABLE : array(INDEX) of ITEM := (INDEX => NULL_ITEM); begin loop select accept READ (N : in INDEX; V : out ITEM) do V := TABLE(N); end READ; or accept WRITE(N : in INDEX; E : in ITEM) do TABLE(N) := E; end WRITE; end select; end loop; end PROTECTED_ARRAY; Note: A task specification specifies the interface of tasks of the task type with other tasks of the same or of different types, and also with the main program. References: declaration 3.1, declarative part 3.9, elaboration 3.9, entry 9.5, entry declaration 9.5, exception handler 11.2, identifier 2.3, main program 10.1, object 3.2, object declaration 3.2.1, representation clause 13.1, reserved word 2.9, sequence of statements 5.1, simple name 4.1, type 3.3, type declaration 3.3.1 9.2 Task Types and Task Objects A task type is a limited type (see 7.4.4). Hence neither assignment nor the predefined comparison for equality and inequality are defined for objects of task types; moreover, the mode out is not allowed for a formal parameter whose type is a task type. A task object is an object whose type is a task type. The value of a task object designates a task that has the entries of the corresponding task type, and whose execution is specified by the corresponding task body. If a task object is the object, or a subcomponent of the object, declared by an object declaration, then the value of the task object is defined by the elaboration of the object declaration. If a task object is the object, or a subcomponent of the object, created by the evaluation of an allocator, then the value of the task object is defined by the evaluation of the allocator. For all parameter modes, if an actual parameter designates a task, the associated formal parameter designates the same task; the same holds for a subcomponent of an actual parameter and the corresponding subcomponent of the associated formal parameter; finally, the same holds for generic parameters. Examples: CONTROL : RESOURCE; TELETYPE : KEYBOARD_DRIVER; POOL : array(1 .. 10) of KEYBOARD_DRIVER; -- see also examples of declarations of single tasks in 9.1 Example of access type designating task objects: type KEYBOARD is access KEYBOARD_DRIVER; TERMINAL : KEYBOARD := new KEYBOARD_DRIVER; Notes: Since a task type is a limited type, it can appear as the definition of a limited private type in a private part, and as a generic actual parameter associated with a formal parameter whose type is a limited type. On the other hand, the type of a generic formal parameter of mode in must not be a limited type and hence cannot be a task type. Task objects behave as constants (a task object always designates the same task) since their values are implicitly defined either at declaration or allocation, or by a parameter association, and since no assignment is available. However the reserved word constant is not allowed in the declaration of a task object since this would require an explicit initialization. A task object that is a formal parameter of mode in is a constant (as is any formal parameter of this mode). If an application needs to store and exchange task identities, it can do so by defining an access type designating the corresponding task objects and by using access values for identification purposes (see above example). Assignment is available for such an access type as for any access type. Subtype declarations are allowed for task types as for other types, but there are no constraints applicable to task types. References: access type 3.8, actual parameter 6.4.1, allocator 4.8, assignment 5.2, component declaration 3.7, composite type 3.3, constant 3.2.1, constant declaration 3.2.1, constraint 3.3, designate 3.8 9.1, elaboration 3.9, entry 9.5, equality operator 4.5.2, formal parameter 6.2, formal parameter mode 6.2, generic actual parameter 12.3, generic association 12.3, generic formal parameter 12.1, generic formal parameter mode 12.1.1, generic unit 12, inequality operator 4.5.2, initialization 3.2.1, limited type 7.4.4, object 3.2, object declaration 3.2.1, parameter association 6.4, private part 7.2, private type 7.4, reserved word 2.9, subcomponent 3.3, subprogram 6, subtype declaration 3.3.2, task body 9.1, type 3.3 9.3 Task Execution - Task Activation A task body defines the execution of any task that is designated by a task object of the corresponding task type. The initial part of this execution is called the activation of the task object, and also that of the designated task; it consists of the elaboration of the declarative part, if any, of the task body. The execution of different tasks, in particular their activation, proceeds in parallel. If an object declaration that declares a task object occurs immediately within a declarative part, then the activation of the task object starts after the elaboration of the declarative part (that is, after passing the reserved word begin following the declarative part); similarly if such a declaration occurs immediately within a package specification, the activation starts after the elaboration of the declarative part of the package body. The same holds for the activation of a task object that is a subcomponent of an object declared immediately within a declarative part or package specification. The first statement following the declarative part is executed only after conclusion of the activation of these task objects. Should an exception be raised by the activation of one of these tasks, that task becomes a completed task (see 9.4); other tasks are not directly affected. Should one of these tasks thus become completed during its activation, the exception TASKING_ERROR is raised upon conclusion of the activation of all of these tasks (whether successfully or not); the exception is raised at a place that is immediately before the first statement following the declarative part (immediately after the reserved word begin). Should several of these tasks thus become completed during their activation, the exception TASKING_ERROR is raised only once. Should an exception be raised by the elaboration of a declarative part or package specification, then any task that is created (directly or indirectly) by this elaboration and that is not yet activated becomes terminated and is therefore never activated (see section 9.4 for the definition of a terminated task). For the above rules, in any package body without statements, a null statement is assumed. For any package without a package body, an implicit package body containing a single null statement is assumed. If a package without a package body is declared immediately within some program unit or block statement, the implicit package body occurs at the end of the declarative part of the program unit or block statement; if there are several such packages, the order of the implicit package bodies is undefined. A task object that is the object, or a subcomponent of the object, created by the evaluation of an allocator is activated by this evaluation. The activation starts after any initialization for the object created by the allocator; if several subcomponents are task objects, they are activated in parallel. The access value designating such an object is returned by the allocator only after the conclusion of these activations. Should an exception be raised by the activation of one of these tasks, that task becomes a completed task; other tasks are not directly affected. Should one of these tasks thus become completed during its activation, the exception TASKING_ERROR is raised upon conclusion of the activation of all of these tasks (whether successfully or not); the exception is raised at the place where the allocator is evaluated. Should several of these tasks thus become completed during their activation, the exception TASKING_ERROR is raised only once. Should an exception be raised by the initialization of the object created by an allocator (hence before the start of any activation), any task designated by a subcomponent of this object becomes terminated and is therefore never activated. Example: procedure P is A, B : RESOURCE; -- elaborate the task objects A, B C : RESOURCE; -- elaborate the task object C begin -- the tasks A, B, C are activated in parallel before the first statement ... end; Notes: An entry of a task can be called before the task has been activated. If several tasks are activated in parallel, the execution of any of these tasks need not await the end of the activation of the other tasks. A task may become completed during its activation either because of an exception or because it is aborted (see 9.10). References: allocator 4.8, completed task 9.4, declarative part 3.9, elaboration 3.9, entry 9.5, exception 11, handling an exception 11.4, package body 7.1, parallel execution 9, statement 5, subcomponent 3.3, task body 9.1, task object 9.2, task termination 9.4, task type 9.1, tasking_error exception 11.1 9.4 Task Dependence - Termination of Tasks Each task depends on at least one master. A master is a construct that is either a task, a currently executing block statement or subprogram, or a library package (a package declared within another program unit is not a master). The dependence on a master is a direct dependence in the following two cases: (a) The task designated by a task object that is the object, or a subcomponent of the object, created by the evaluation of an allocator depends on the master that elaborates the corresponding access type definition. (b) The task designated by any other task object depends on the master whose execution creates the task object. Furthermore, if a task depends on a given master that is a block statement executed by another master, then the task depends also on this other master, in an indirect manner; the same holds if the given master is a subprogram called by another master, and if the given master is a task that depends (directly or indirectly) on another master. Dependences exist for objects of a private type whose full declaration is in terms of a task type. A task is said to have completed its execution when it has finished the execution of the sequence of statements that appears after the reserved word begin in the corresponding body. Similarly a block or a subprogram is said to have completed its execution when it has finished the execution of the corresponding sequence of statements. For a block statement, the execution is also said to be completed when it reaches an exit, return, or goto statement transferring control out of the block. For a procedure, the execution is also said to be completed when a corresponding return statement is reached. For a function, the execution is also said to be completed after the evaluation of the result expression of a return statement. Finally the execution of a task, block statement, or subprogram is completed if an exception is raised by the execution of its sequence of statements and there is no corresponding handler, or, if there is one, when it has finished the execution of the corresponding handler. If a task has no dependent task, its termination takes place when it has completed its execution. After its termination, a task is said to be terminated. If a task has dependent tasks, its termination takes place when the execution of the task is completed and all dependent tasks are terminated. A block statement or subprogram body whose execution is completed is not left until all of its dependent tasks are terminated. Termination of a task otherwise takes place if and only if its execution has reached an open terminate alternative in a select statement (see 9.7.1), and the following conditions are satisfied: - The task depends on some master whose execution is completed (hence not a library package). - Each task that depends on the master considered is either already terminated or similarly waiting on an open terminate alternative of a select statement. When both conditions are satisfied, the task considered becomes terminated, together with all tasks that depend on the master considered. Example: declare type GLOBAL is access RESOURCE; -- see 9.1 A, B : RESOURCE; G : GLOBAL; begin -- activation of A and B declare type LOCAL is access RESOURCE; X : GLOBAL := new RESOURCE; -- activation of X.all L : LOCAL := new RESOURCE; -- activation of L.all C : RESOURCE; begin -- activation of C G := X; -- both G and X designate the same task object ... end; -- await termination of C and L.all (but not X.all) ... end; -- await termination of A, B, and G.all Notes: The rules given for termination imply that all tasks that depend (directly or indirectly) on a given master and that are not already terminated, can be terminated (collectively) if and only if each of them is waiting on an open terminate alternative of a select statement and the execution of the given master is completed. The usual rules apply to the main program. Consequently, termination of the main program awaits termination of any dependent task even if the corresponding task type is declared in a library package. On the other hand, termination of the main program does not await termination of tasks that depend on library packages; the language does not define whether such tasks are required to terminate. For an access type derived from another access type, the corresponding access type definition is that of the parent type; the dependence is on the master that elaborates the ultimate parent access type definition. A renaming declaration defines a new name for an existing entity and hence creates no further dependence. References: access type 3.8, allocator 4.8, block statement 5.6, declaration 3.1, designate 3.8 9.1, exception 11, exception handler 11.2, exit statement 5.7, function 6.5, goto statement 5.9, library unit 10.1, main program 10.1, object 3.2, open alternative 9.7.1, package 7, program unit 6, renaming declaration 8.5, return statement 5.8, selective wait 9.7.1, sequence of statements 5.1, statement 5, subcomponent 3.3, subprogram body 6.3, subprogram call 6.4, task body 9.1, task object 9.2, terminate alternative 9.7.1 9.5 Entries, Entry Calls, and Accept Statements Entry calls and accept statements are the primary means of synchronization of tasks, and of communicating values between tasks. An entry declaration is similar to a subprogram declaration and is only allowed in a task specification. The actions to be performed when an entry is called are specified by corresponding accept statements. entry_declaration ::= entry identifier [(discrete_range)] [formal_part]; entry_call_statement ::= entry_name [actual_parameter_part]; accept_statement ::= accept entry_simple_name [(entry_index)] [formal_part] [do sequence_of_statements end [entry_simple_name]]; entry_index ::= expression An entry declaration that includes a discrete range (see 3.6.1) declares a family of distinct entries having the same formal part (if any); that is, one such entry for each value of the discrete range. The term single entry is used in the definition of any rule that applies to any entry other than one of a family. The task designated by an object of a task type has (or owns) the entries declared in the specification of the task type. Within the body of a task, each of its single entries or entry families can be named by the corresponding simple name. The name of an entry of a family takes the form of an indexed component, the family simple name being followed by the index in parentheses; the type of this index must be the same as that of the discrete range in the corresponding entry family declaration. Outside the body of a task an entry name has the form of a selected component, whose prefix denotes the task object, and whose selector is the simple name of one of its single entries or entry families. A single entry overloads a subprogram, an enumeration literal, or another single entry if they have the same identifier. Overloading is not defined for entry families. A single entry or an entry of an entry family can be renamed as a procedure as explained in section 8.5. The parameter modes defined for parameters of the formal part of an entry declaration are the same as for a subprogram declaration and have the same meaning (see 6.2). The syntax of an entry call statement is similar to that of a procedure call statement, and the rules for parameter associations are the same as for subprogram calls (see 6.4.1 and 6.4.2). An accept statement specifies the actions to be performed at a call of a named entry (it can be an entry of a family). The formal part of an accept statement must conform to the formal part given in the declaration of the single entry or entry family named by the accept statement (see section 6.3.1 for the conformance rules). If a simple name appears at the end of an accept statement, it must repeat that given at the start. An accept statement for an entry of a given task is only allowed within the corresponding task body; excluding within the body of any program unit that is, itself, inner to the task body; and excluding within another accept statement for either the same single entry or an entry of the same family. (One consequence of this rule is that a task can execute accept statements only for its own entries.) A task body can contain more than one accept statement for the same entry. For the elaboration of an entry declaration, the discrete range, if any, is evaluated and the formal part, if any, is then elaborated as for a subprogram declaration. Execution of an accept statement starts with the evaluation of the entry index (in the case of an entry of a family). Execution of an entry call statement starts with the evaluation of the entry name; this is followed by any evaluations required for actual parameters in the same manner as for a subprogram call (see 6.4). Further execution of an accept statement and of a corresponding entry call statement are synchronized. If a given entry is called by only one task, there are two possibilities: - If the calling task issues an entry call statement before a corresponding accept statement is reached by the task owning the entry, the execution of the calling task is suspended. - If a task reaches an accept statement prior to any call of that entry, the execution of the task is suspended until such a call is received. When an entry has been called and a corresponding accept statement has been reached, the sequence of statements, if any, of the accept statement is executed by the called task (while the calling task remains suspended). This interaction is called a rendezvous. Thereafter, the calling task and the task owning the entry continue their execution in parallel. If several tasks call the same entry before a corresponding accept statement is reached, the calls are queued; there is one queue associated with each entry. Each execution of an accept statement removes one call from the queue. The calls are processed in the order of arrival. An attempt to call an entry of a task that has completed its execution raises the exception TASKING_ERROR at the point of the call, in the calling task; similarly, this exception is raised at the point of the call if the called task completes its execution before accepting the call (see also 9.10 for the case when the called task becomes abnormal). The exception CONSTRAINT_ERROR is raised if the index of an entry of a family is not within the specified discrete range. Examples of entry declarations: entry READ(V : out ITEM); entry SEIZE; entry REQUEST(LEVEL)(D : ITEM); -- a family of entries Examples of entry calls: CONTROL.RELEASE; -- see 9.2 and 9.1 PRODUCER_CONSUMER.WRITE(E); -- see 9.1 POOL(5).READ(NEXT_CHAR); -- see 9.2 and 9.1 CONTROLLER.REQUEST(LOW)(SOME_ITEM); -- see 9.1 Examples of accept statements: accept SEIZE; accept READ(V : out ITEM) do V := LOCAL_ITEM; end READ; accept REQUEST(LOW)(D : ITEM) do ... end REQUEST; Notes: The formal part given in an accept statement is not elaborated; it is only used to identify the corresponding entry. An accept statement can call subprograms that issue entry calls. An accept statement need not have a sequence of statements even if the corresponding entry has parameters. Equally, it can have a sequence of statements even if the corresponding entry has no parameters. The sequence of statements of an accept statement can include return statements. A task can call its own entries but it will, of course, deadlock. The language permits conditional and timed entry calls (see 9.7.2 and 9.7.3). The language rules ensure that a task can only be in one entry queue at a given time. If the bounds of the discrete range of an entry family are integer literals, the index (in an entry name or accept statement) must be of the predefined type INTEGER (see 3.6.1). References: abnormal task 9.10, actual parameter part 6.4, completed task 9.4, conditional entry call 9.7.2, conformance rules 6.3.1, constraint_error exception 11.1, designate 9.1, discrete range 3.6.1, elaboration 3.1 3.9, enumeration literal 3.5.1, evaluation 4.5, expression 4.4, formal part 6.1, identifier 2.3, indexed component 4.1.1, integer type 3.5.4, name 4.1, object 3.2, overloading 6.6 8.7, parallel execution 9, prefix 4.1, procedure 6, procedure call 6.4, renaming declaration 8.5, return statement 5.8, scope 8.2, selected component 4.1.3, selector 4.1.3, sequence of statements 5.1, simple expression 4.4, simple name 4.1, subprogram 6, subprogram body 6.3, subprogram declaration 6.1, task 9, task body 9.1, task specification 9.1, tasking_error exception 11.1, timed entry call 9.7.3 9.6 Delay Statements, Duration, and Time The execution of a delay statement evaluates the simple expression, and suspends further execution of the task that executes the delay statement, for at least the duration specified by the resulting value. delay_statement ::= delay simple_expression; The simple expression must be of the predefined fixed point type DURATION; its value is expressed in seconds; a delay statement with a negative value is equivalent to a delay statement with a zero value. Any implementation of the type DURATION must allow representation of durations (both positive and negative) up to at least 86400 seconds (one day); the smallest representable duration, DURATION'SMALL must not be greater than twenty milliseconds (whenever possible, a value not greater than fifty microseconds should be chosen). Note that DURATION'SMALL need not correspond to the basic clock cycle, the named number SYSTEM.TICK (see 13.7). The definition of the type TIME is provided in the predefined library package CALENDAR. The function CLOCK returns the current value of TIME at the time it is called. The functions YEAR, MONTH, DAY and SECONDS return the corresponding values for a given value of the type TIME; the procedure SPLIT returns all four corresponding values. Conversely, the function TIME_OF combines a year number, a month number, a day number, and a duration, into a value of type TIME. The operators "+" and "-" for addition and subtraction of times and durations, and the relational operators for times, have the conventional meaning. The exception TIME_ERROR is raised by the function TIME_OF if the actual parameters do not form a proper date. This exception is also raised by the operators "+" and "-" if, for the given operands, these operators cannot return a date whose year number is in the range of the corresponding subtype, or if the operator "-" cannot return a result that is in the range of the type DURATION. package CALENDAR is type TIME is private; subtype YEAR_NUMBER is INTEGER range 1901 .. 2099; subtype MONTH_NUMBER is INTEGER range 1 .. 12; subtype DAY_NUMBER is INTEGER range 1 .. 31; subtype DAY_DURATION is DURATION range 0.0 .. 86_400.0; function CLOCK return TIME; function YEAR (DATE : TIME) return YEAR_NUMBER; function MONTH (DATE : TIME) return MONTH_NUMBER; function DAY (DATE : TIME) return DAY_NUMBER; function SECONDS(DATE : TIME) return DAY_DURATION; procedure SPLIT (DATE : in TIME; YEAR : out YEAR_NUMBER; MONTH : out MONTH_NUMBER; DAY : out DAY_NUMBER; SECONDS : out DAY_DURATION); function TIME_OF(YEAR : YEAR_NUMBER; MONTH : MONTH_NUMBER; DAY : DAY_NUMBER; SECONDS : DAY_DURATION := 0.0) return TIME; function "+" (LEFT : TIME; RIGHT : DURATION) return TIME; function "+" (LEFT : DURATION; RIGHT : TIME) return TIME; function "-" (LEFT : TIME; RIGHT : DURATION) return TIME; function "-" (LEFT : TIME; RIGHT : TIME) return DURATION; function "<" (LEFT, RIGHT : TIME) return BOOLEAN; function "<=" (LEFT, RIGHT : TIME) return BOOLEAN; function ">" (LEFT, RIGHT : TIME) return BOOLEAN; function ">=" (LEFT, RIGHT : TIME) return BOOLEAN; TIME_ERROR : exception; -- can be raised by TIME_OF, "+", and "-" private -- implementation-dependent end; Examples: delay 3.0; -- delay 3.0 seconds declare use CALENDAR; -- INTERVAL is a global constant of type DURATION NEXT_TIME : TIME := CLOCK + INTERVAL; begin loop delay NEXT_TIME - CLOCK; -- some actions NEXT_TIME := NEXT_TIME + INTERVAL; end loop; end; Notes: The second example causes the loop to be repeated every INTERVAL seconds on average. This interval between two successive iterations is only approximate. However, there will be no cumulative drift as long as the duration of each iteration is (sufficiently) less than INTERVAL. References: adding operator 4.5, duration C, fixed point type 3.5.9, function call 6.4, library unit 10.1, operator 4.5, package 7, private type 7.4, relational operator 4.5, simple expression 4.4, statement 5, task 9, type 3.3 9.7 Select Statements There are three forms of select statements. One form provides a selective wait for one or more alternatives. The other two provide conditional and timed entry calls. select_statement ::= selective_wait | conditional_entry_call | timed_entry_call References: selective wait 9.7.1, conditional entry call 9.7.2, timed entry call 9.7.3 9.7.1 Selective Waits This form of the select statement allows a combination of waiting for, and selecting from, one or more alternatives. The selection can depend on conditions associated with each alternative of the selective wait. selective_wait ::= select select_alternative {or select_alternative} [else sequence_of_statements] end select; select_alternative ::= [when condition =>] selective_wait_alternative selective_wait_alternative ::= accept_alternative | delay_alternative | terminate_alternative accept_alternative ::= accept_statement [sequence_of_statements] delay_alternative ::= delay_statement [sequence_of_statements] terminate_alternative ::= terminate; A selective wait must contain at least one accept alternative. In addition a selective wait can contain either a terminate alternative (only one), or one or more delay alternatives, or an else part; these three possibilities are mutually exclusive. A select alternative is said to be open if it does not start with when and a condition, or if the condition is TRUE. It is said to be closed otherwise. For the execution of a selective wait, any conditions specified after when are evaluated in some order that is not defined by the language; open alternatives are thus determined. For an open delay alternative, the delay expression is also evaluated. Similarly, for an open accept alternative for an entry of a family, the entry index is also evaluated. Selection and execution of one open alternative, or of the else part, then completes the execution of the selective wait; the rules for this selection are described below. Open accept alternatives are first considered. Selection of one such alternative takes place immediately if a corresponding rendezvous is possible, that is, if there is a corresponding entry call issued by another task and waiting to be accepted. If several alternatives can thus be selected, one of them is selected arbitrarily (that is, the language does not define which one). When such an alternative is selected, the corresponding accept statement and possible subsequent statements are executed. If no rendezvous is immediately possible and there is no else part, the task waits until an open selective wait alternative can be selected. Selection of the other forms of alternative or of an else part is performed as follows: - An open delay alternative will be selected if no accept alternative can be selected before the specified delay has elapsed (immediately, for a negative or zero delay in the absence of queued entry calls); any subsequent statements of the alternative are then executed. If several delay alternatives can thus be selected (that is, if they have the same delay), one of them is selected arbitrarily. - The else part is selected and its statements are executed if no accept alternative can be immediately selected, in particular, if all alternatives are closed. - An open terminate alternative is selected if the conditions stated in section 9.4 are satisfied. It is a consequence of other rules that a terminate alternative cannot be selected while there is a queued entry call for any entry of the task. The exception PROGRAM_ERROR is raised if all alternatives are closed and there is no else part. Examples of a select statement: select accept DRIVER_AWAKE_SIGNAL; or delay 30.0*SECONDS; STOP_THE_TRAIN; end select; Example of a task body with a select statement: task body RESOURCE is BUSY : BOOLEAN := FALSE; begin loop select when not BUSY => accept SEIZE do BUSY := TRUE; end; or accept RELEASE do BUSY := FALSE; end; or terminate; end select; end loop; end RESOURCE; Notes: A selective wait is allowed to have several open delay alternatives. A selective wait is allowed to have several open accept alternatives for the same entry. References: accept statement 9.5, condition 5.3, declaration 3.1, delay expression 9.6, delay statement 9.6, duration 9.6, entry 9.5, entry call 9.5, entry index 9.5, program_error exception 11.1, queued entry call 9.5, rendezvous 9.5, select statement 9.7, sequence of statements 5.1, task 9 9.7.2 Conditional Entry Calls A conditional entry call issues an entry call that is then canceled if a rendezvous is not immediately possible. conditional_entry_call ::= select entry_call_statement [sequence_of_statements] else sequence_of_statements end select; For the execution of a conditional entry call, the entry name is first evaluated. This is followed by any evaluations required for actual parameters as in the case of a subprogram call (see 6.4). The entry call is canceled if the execution of the called task has not reached a point where it is ready to accept the call (that is, either an accept statement for the corresponding entry, or a select statement with an open accept alternative for the entry), or if there are prior queued entry calls for this entry. If the called task has reached a select statement, the entry call is canceled if an accept alternative for this entry is not selected. If the entry call is canceled, the statements of the else part are executed. Otherwise, the rendezvous takes place; and the optional sequence of statements after the entry call is then executed. The execution of a conditional entry call raises the exception TASKING_ERROR if the called task has already completed its execution (see also 9.10 for the case when the called task becomes abnormal). Example: procedure SPIN(R : RESOURCE) is begin loop select R.SEIZE; return; else null; -- busy waiting end select; end loop; end; References: abnormal task 9.10, accept statement 9.5, actual parameter part 6.4, completed task 9.4, entry call statement 9.5, entry family 9.5, entry index 9.5, evaluation 4.5, expression 4.4, open alternative 9.7.1, queued entry call 9.5, rendezvous 9.5, select statement 9.7, sequence of statements 5.1, task 9, tasking_error exception 11.1 9.7.3 Timed Entry Calls A timed entry call issues an entry call that is canceled if a rendezvous is not started within a given delay. timed_entry_call ::= select entry_call_statement [sequence_of_statements] or delay_alternative end select; For the execution of a timed entry call, the entry name is first evaluated. This is followed by any evaluations required for actual parameters as in the case of a subprogram call (see 6.4). The expression stating the delay is then evaluated, and the entry call is finally issued. If a rendezvous can be started within the specified duration (or immediately, as for a conditional entry call, for a negative or zero delay), it is performed and the optional sequence of statements after the entry call is then executed. Otherwise, the entry call is canceled when the specified duration has expired, and the optional sequence of statements of the delay alternative is executed. The execution of a timed entry call raises the exception TASKING_ERROR if the called task completes its execution before accepting the call (see also 9.10 for the case when the called task becomes abnormal). Example: select CONTROLLER.REQUEST(MEDIUM)(SOME_ITEM); or delay 45.0; -- controller too busy, try something else end select; References: abnormal task 9.10, accept statement 9.5, actual parameter part 6.4, completed task 9.4, conditional entry call 9.7.2, delay expression 9.6, delay statement 9.6, duration 9.6, entry call statement 9.5, entry family 9.5, entry index 9.5, evaluation 4.5, expression 4.4, rendezvous 9.5, sequence of statements 5.1, task 9, tasking_error exception 11.1 9.8 Priorities Each task may (but need not) have a priority, which is a value of the subtype PRIORITY (of the type INTEGER) declared in the predefined library package SYSTEM (see 13.7). A lower value indicates a lower degree of urgency; the range of priorities is implementation-defined. A priority is associated with a task if a pragma pragma PRIORITY (static_expression); appears in the corresponding task specification; the priority is given by the value of the expression. A priority is associated with the main program if such a pragma appears in its outermost declarative part. At most one such pragma can appear within a given task specification or for a subprogram that is a library unit, and these are the only allowed places for this pragma. A pragma PRIORITY has no effect if it occurs in a subprogram other than the main program. The specification of a priority is an indication given to assist the implementation in the allocation of processing resources to parallel tasks when there are more tasks eligible for execution than can be supported simultaneously by the available processing resources. The effect of priorities on scheduling is defined by the following rule: If two tasks with different priorities are both eligible for execution and could sensibly be executed using the same physical processors and the same other processing resources, then it cannot be the case that the task with the lower priority is executing while the task with the higher priority is not. For tasks of the same priority, the scheduling order is not defined by the language. For tasks without explicit priority, the scheduling rules are not defined, except when such tasks are engaged in a rendezvous. If the priorities of both tasks engaged in a rendezvous are defined, the rendezvous is executed with the higher of the two priorities. If only one of the two priorities is defined, the rendezvous is executed with at least that priority. If neither is defined, the priority of the rendezvous is undefined. Notes: The priority of a task is static and therefore fixed. However, the priority during a rendezvous is not necessarily static since it also depends on the priority of the task calling the entry. Priorities should be used only to indicate relative degrees of urgency; they should not be used for task synchronization. References: declarative part 3.9, entry call statement 9.5, integer type 3.5.4, main program 10.1, package system 13.7, pragma 2.8, rendezvous 9.5, static expression 4.9, subtype 3.3, task 9, task specification 9.1 9.9 Task and Entry Attributes For a task object or value T the following attributes are defined: T'CALLABLE Yields the value FALSE when the execution of the task designated by T is either completed or terminated, or when the task is abnormal. Yields the value TRUE otherwise. The value of this attribute is of the predefined type BOOLEAN. T'TERMINATED Yields the value TRUE if the task designated by T is terminated. Yields the value FALSE otherwise. The value of this attribute is of the predefined type BOOLEAN. In addition, the representation attributes STORAGE_SIZE, SIZE, and ADDRESS are defined for a task object T or a task type T (see 13.7.2). The attribute COUNT is defined for an entry E of a task unit T. The entry can be either a single entry or an entry of a family (in either case the name of the single entry or entry family can be either a simple or an expanded name). This attribute is only allowed within the body of T, but excluding within any program unit that is, itself, inner to the body of T. E'COUNT Yields the number of entry calls presently queued on the entry E (if the attribute is evaluated by the execution of an accept statement for the entry E, the count does not include the calling task). The value of this attribute is of the type universal_integer. Note: Algorithms interrogating the attribute E'COUNT should take precautions to allow for the increase of the value of this attribute for incoming entry calls, and its decrease, for example with timed entry calls. References: abnormal task 9.10, accept statement 9.5, attribute 4.1.4, boolean type 3.5.3, completed task 9.4, designate 9.1, entry 9.5, false boolean value 3.5.3, queue of entry calls 9.5, storage unit 13.7, task 9, task object 9.2, task type 9.1, terminated task 9.4, timed entry call 9.7.3, true boolean value 3.5.3, universal_integer type 3.5.4 9.10 Abort Statements An abort statement causes one or more tasks to become abnormal, thus preventing any further rendezvous with such tasks. abort_statement ::= abort task_name {, task_name}; The determination of the type of each task name uses the fact that the type of the name is a task type. For the execution of an abort statement, the given task names are evaluated in some order that is not defined by the language. Each named task then becomes abnormal unless it is already terminated; similarly, any task that depends on a named task becomes abnormal unless it is already terminated. Any abnormal task whose execution is suspended at an accept statement, a select statement, or a delay statement becomes completed; any abnormal task whose execution is suspended at an entry call, and that is not yet in a corresponding rendezvous, becomes completed and is removed from the entry queue; any abnormal task that has not yet started its activation becomes completed (and hence also terminated). This completes the execution of the abort statement. The completion of any other abnormal task need not happen before completion of the abort statement. It must happen no later than when the abnormal task reaches a synchronization point that is one of the following: the end of its activation; a point where it causes the activation of another task; an entry call; the start or the end of an accept statement; a select statement; a delay statement; an exception handler; or an abort statement. If a task that calls an entry becomes abnormal while in a rendezvous, its termination does not take place before the completion of the rendezvous (see 11.5). The call of an entry of an abnormal task raises the exception TASKING_ERROR at the place of the call. Similarly, the exception TASKING_ERROR is raised for any task that has called an entry of an abnormal task, if the entry call is still queued or if the rendezvous is not yet finished (whether the entry call is an entry call statement, or a conditional or timed entry call); the exception is raised no later than the completion of the abnormal task. The value of the attribute CALLABLE is FALSE for any task that is abnormal (or completed). If the abnormal completion of a task takes place while the task updates a variable, then the value of this variable is undefined. Example: abort USER, TERMINAL.all, POOL(3); Notes: An abort statement should be used only in extremely severe situations requiring unconditional termination. A task is allowed to abort any task, including itself. References: abnormal in rendezvous 11.5, accept statement 9.5, activation 9.3, attribute 4.1.4, callable (predefined attribute) 9.9, conditional entry call 9.7.2, delay statement 9.6, dependent task 9.4, entry call statement 9.5, evaluation of a name 4.1, exception handler 11.2, false boolean value 3.5.3, name 4.1, queue of entry calls 9.5, rendezvous 9.5, select statement 9.7, statement 5, task 9, tasking_error exception 11.1, terminated task 9.4, timed entry call 9.7.3 9.11 Shared Variables The normal means of communicating values between tasks is by entry calls and accept statements. If two tasks read or update a shared variable (that is, a variable accessible by both), then neither of them may assume anything about the order in which the other performs its operations, except at the points where they synchronize. Two tasks are synchronized at the start and at the end of their rendezvous. At the start and at the end of its activation, a task is synchronized with the task that causes this activation. A task that has completed its execution is synchronized with any other task. For the actions performed by a program that uses shared variables, the following assumptions can always be made: - If between two synchronization points of a task, this task reads a shared variable whose type is a scalar or access type, then the variable is not updated by any other task at any time between these two points. - If between two synchronization points of a task, this task updates a shared variable whose type is a scalar or access type, then the variable is neither read nor updated by any other task at any time between these two points. The execution of the program is erroneous if any of these assumptions is violated. If a given task reads the value of a shared variable, the above assumptions allow an implementation to maintain local copies of the value (for example, in registers or in some other form of temporary storage); and for as long as the given task neither reaches a synchronization point nor updates the value of the shared variable, the above assumptions imply that, for the given task, reading a local copy is equivalent to reading the shared variable itself. Similarly, if a given task updates the value of a shared variable, the above assumptions allow an implementation to maintain a local copy of the value, and to defer the effective store of the local copy into the shared variable until a synchronization point, provided that every further read or update of the variable by the given task is treated as a read or update of the local copy. On the other hand, an implementation is not allowed to introduce a store, unless this store would also be executed in the canonical order (see 11.6). The pragma SHARED can be used to specify that every read or update of a variable is a synchronization point for that variable; that is, the above assumptions always hold for the given variable (but not necessarily for other variables). The form of this pragma is as follows: pragma SHARED(variable_simple_name); This pragma is allowed only for a variable declared by an object declaration and whose type is a scalar or access type; the variable declaration and the pragma must both occur (in this order) immediately within the same declarative part or package specification; the pragma must appear before any occurrence of the name of the variable, other than in an address clause. An implementation must restrict the objects for which the pragma SHARED is allowed to objects for which each of direct reading and direct updating is implemented as an indivisible operation. References: accept statement 9.5, activation 9.3, assignment 5.2, canonical order 11.6, declarative part 3.9, entry call statement 9.5, erroneous 1.6, global 8.1, package specification 7.1, pragma 2.8, read a value 6.2, rendezvous 9.5, simple name 3.1 4.1, task 9, type 3.3, update a value 6.2, variable 3.2.1 9.12 Example of Tasking The following example defines a buffering task to smooth variations between the speed of output of a producing task and the speed of input of some consuming task. For instance, the producing task may contain the statements loop -- produce the next character CHAR BUFFER.WRITE(CHAR); exit when CHAR = ASCII.EOT; end loop; and the consuming task may contain the statements loop BUFFER.READ(CHAR); -- consume the character CHAR exit when CHAR = ASCII.EOT; end loop; The buffering task contains an internal pool of characters processed in a round-robin fashion. The pool has two indices, an IN_INDEX denoting the space for the next input character and an OUT_INDEX denoting the space for the next output character. task BUFFER is entry READ (C : out CHARACTER); entry WRITE(C : in CHARACTER); end; task body BUFFER is POOL_SIZE : constant INTEGER := 100; POOL : array(1 .. POOL_SIZE) of CHARACTER; COUNT : INTEGER range 0 .. POOL_SIZE := 0; IN_INDEX, OUT_INDEX : INTEGER range 1 .. POOL_SIZE := 1; begin loop select when COUNT < POOL_SIZE => accept WRITE(C : in CHARACTER) do POOL(IN_INDEX) := C; end; IN_INDEX := IN_INDEX mod POOL_SIZE + 1; COUNT := COUNT + 1; or when COUNT > 0 => accept READ(C : out CHARACTER) do C := POOL(OUT_INDEX); end; OUT_INDEX := OUT_INDEX mod POOL_SIZE + 1; COUNT := COUNT - 1; or terminate; end select; end loop; end BUFFER;