ada task entry

Tasks and protected objects allow the implementation of concurrency in Ada. The following sections explain these concepts in more detail.

A task can be thought as an application that runs concurrently with the main application. In other programming languages, a task might be called a thread , and tasking might be called multithreading .

Tasks may synchronize with the main application but may also process information completely independently from the main application. Here we show how this is accomplished.

Simple task 

Tasks are declared using the keyword task . The task implementation is specified in a task body block. For example:

Here, we're declaring and implementing the task T . As soon as the main application starts, task T starts automatically — it's not necessary to manually start this task. By running the application above, we can see that both calls to Put_Line are performed.

The main application is itself a task (the main or “environment” task).

In this example, the subprogram Show_Simple_Task is the main task of the application.

Task T is a subtask.

Each subtask has a master, which represents the program construct in which the subtask is declared. In this case, the main subprogram Show_Simple_Task is T 's master.

The master construct is executed by some enclosing task, which we will refer to as the "master task" of the subtask.

The number of tasks is not limited to one: we could include a task T2 in the example above.

This task also starts automatically and runs concurrently with both task T and the main task. For example:

Simple synchronization 

As we've just seen, as soon as the master construct reaches its “begin”, its subtasks also start automatically. The master continues its processing until it has nothing more to do. At that point, however, it will not terminate. Instead, the master waits until its subtasks have finished before it allows itself to complete. In other words, this waiting process provides synchronization between the master task and its subtasks. After this synchronization, the master construct will complete. For example:

The same mechanism is used for other subprograms that contain subtasks: the subprogram execution will wait for its subtasks to finish. So this mechanism is not limited to the main subprogram and also applies to any subprogram called by the main subprogram, directly or indirectly.

Synchronization also occurs if we move the task to a separate package. In the example below, we declare a task T in the package Simple_Sync_Pkg .

This is the corresponding package body:

Because the package is with 'ed by the main procedure, the task T defined in the package will become a subtask of the main task. For example:

As soon as the main subprogram returns, the main task synchronizes with any subtasks spawned by packages T from Simple_Sync_Pkg before finally terminating.

We can introduce a delay by using the keyword delay . This puts the current task to sleep for the length of time (in seconds) specified in the delay statement. For example:

In this example, we're making the task T wait one second after each time it displays the "hello" message. In addition, the main task is waiting 1.5 seconds before displaying its own "hello" message

Synchronization: rendezvous 

The only type of synchronization we've seen so far is the one that happens automatically at the end of a master construct with a subtask. You can also define custom synchronization points using the keyword entry . An entry can be viewed as a special kind of subprogram, which is called by another task using a similar syntax, as we will see later.

In the task body definition, you define which part of the task will accept the entries by using the keyword accept . A task proceeds until it reaches an accept statement and then waits for some other task to synchronize with it. Specifically,

The task with the entry waits at that point (in the accept statement), ready to accept a call to the corresponding entry from the master task.

The other task calls the task entry, in a manner similar to a procedure call, to synchronize with the entry.

This synchronization between tasks is called a rendezvous . Let's see an example:

In this example, we declare an entry Start for task T . In the task body, we implement this entry using accept Start . When task T reaches this point, it waits for some other task to call its entry. This synchronization occurs in the T . Start statement. After the rendezvous completes, the main task and task T again run concurrently until they synchronize one final time when the main subprogram Show_Rendezvous finishes.

An entry may be used to perform more than a simple task synchronization: it also may perform multiple statements during the time both tasks are synchronized. We do this with a do ... end block. For the previous example, we would simply write accept Start do < statements > ; end ; . We use this kind of block in the next example.

Select loop 

There's no limit to the number of times an entry can be accepted. We could even create an infinite loop in the task and accept calls to the same entry over and over again. An infinite loop, however, prevents the subtask from finishing, so it blocks its master task when it reaches the end of its processing. Therefore, a loop containing accept statements in a task body can be used in conjunction with a select ... or terminate statement. In simple terms, this statement allows its master task to automatically terminate the subtask when the master construct reaches its end. For example:

In this example, the task body implements an infinite loop that accepts calls to the Reset and Increment entry. We make the following observations:

The accept E do ... end block is used to increment a counter.

As long as task T is performing the do ... end block, the main task waits for the block to complete.

The main task is calling the Increment entry multiple times in the loop from 1 .. 4 . It is also calling the Reset entry before the second loop.

Because task T contains an infinite loop, it always accepts calls to the Reset and Increment entries. When the master construct of the subtask (the Show_Rendezvous_Loop subprogram) completes, it checks the status of the T task. Even though task T could accept new calls to the Reset or Increment entries, the master construct is allowed to terminate task T due to the or terminate part of the select statement.

Cycling tasks 

In a previous example, we saw how to delay a task a specified time by using the delay keyword. However, using delay statements in a loop is not enough to guarantee regular intervals between those delay statements. For example, we may have a call to a computationally intensive procedure between executions of successive delay statements:

In this case, we can't guarantee that exactly 10 seconds have elapsed after 10 calls to the delay statement because a time drift may be introduced by the Computational_Intensive_App procedure. In many cases, this time drift is not relevant, so using the delay keyword is good enough.

However, there are situations where a time drift isn't acceptable. In those cases, we need to use the delay until statement, which accepts a precise time for the end of the delay, allowing us to define a regular interval. This is useful, for example, in real-time applications.

We will soon see an example of how this time drift may be introduced and how the delay until statement circumvents the problem. But before we do that, we look at a package containing a procedure allowing us to measure the elapsed time ( Show_Elapsed_Time ) and a dummy Computational_Intensive_App procedure which is simulated by using a simple delay. This is the complete package:

Using this auxiliary package, we're now ready to write our time-drifting application:

We can see by running the application that we already have a time difference of about four seconds after three iterations of the loop due to the drift introduced by Computational_Intensive_App . Using the delay until statement, however, we're able to avoid this time drift and have a regular interval of exactly one second:

Now, as we can see by running the application, the delay until statement ensures that the Computational_Intensive_App doesn't disturb the regular interval of one second between iterations.

Protected objects 

When multiple tasks are accessing shared data, corruption of that data may occur. For example, data may be inconsistent if one task overwrites parts of the information that's being read by another task at the same time. In order to avoid these kinds of problems and ensure information is accessed in a coordinated way, we use protected objects .

Protected objects encapsulate data and provide access to that data by means of protected operations , which may be subprograms or protected entries. Using protected objects ensures that data is not corrupted by race conditions or other concurrent access.

Objects can be protected from concurrent access using Ada tasks. In fact, this was the only way of protecting objects from concurrent access in Ada 83 (the first version of the Ada language). However, the use of protected objects is much simpler than using similar mechanisms implemented using only tasks. Therefore, you should use protected objects when your main goal is only to protect data.

Simple object 

You declare a protected object with the protected keyword. The syntax is similar to that used for packages: you can declare operations (e.g., procedures and functions) in the public part and data in the private part. The corresponding implementation of the operations is included in the protected body of the object. For example:

In this example, we define two operations for Obj : Set and Get . The implementation of these operations is in the Obj body. The syntax used for writing these operations is the same as that for normal procedures and functions. The implementation of protected objects is straightforward — we simply access and update Local in these subprograms. To call these operations in the main application, we use prefixed notation, e.g., Obj . Get .

In addition to protected procedures and functions, you can also define protected entry points. Do this using the entry keyword. Protected entry points allow you to define barriers using the when keyword. Barriers are conditions that must be fulfilled before the entry can start performing its actual processing — we speak of releasing the barrier when the condition is fulfilled.

The previous example used procedures and functions to define operations on the protected objects. However, doing so permits reading protected information (via Obj . Get ) before it's set (via Obj . Set ). To allow that to be a defined operation, we specified a default value (0). Instead, by rewriting Obj . Get using an entry instead of a function, we implement a barrier, ensuring no task can read the information before it's been set.

The following example implements the barrier for the Obj . Get operation. It also contains two concurrent subprograms (main task and task T ) that try to access the protected object.

As we see by running it, the main application waits until the protected object is set (by the call to Obj . Set in task T ) before it reads the information (via Obj . Get ). Because a 4-second delay has been added in task T , the main application is also delayed by 4 seconds. Only after this delay does task T set the object and release the barrier in Obj . Get so that the main application can then resume processing (after the information is retrieved from the protected object).

Task and protected types 

In the previous examples, we defined single tasks and protected objects. We can, however, generalize tasks and protected objects using type definitions. This allows us, for example, to create multiple tasks based on just a single task type.

Task types 

A task type is a generalization of a task. The declaration is similar to simple tasks: you replace task with task type . The difference between simple tasks and task types is that task types don't create actual tasks that automatically start. Instead, a task object declaration is needed. This is exactly the way normal variables and types work: objects are only created by variable definitions, not type definitions.

To illustrate this, we repeat our first example:

We now rewrite it by replacing task T with task type TT . We declare a task ( A_Task ) based on the task type TT after its definition:

We can extend this example and create an array of tasks. Since we're using the same syntax as for variable declarations, we use a similar syntax for task types: array (<>) of Task _ Type . Also, we can pass information to the individual tasks by defining a Start entry. Here's the updated example:

In this example, we're declaring five tasks in the array My_Tasks . We pass the array index to the individual tasks in the entry point ( Start ). After the synchronization between the individual subtasks and the main task, each subtask calls Put_Line concurrently.

Protected types 

A protected type is a generalization of a protected object. The declaration is similar to that for protected objects: you replace protected with protected type . Like task types, protected types require an object declaration to create actual objects. Again, this is similar to variable declarations and allows for creating arrays (or other composite objects) of protected objects.

We can reuse a previous example and rewrite it to use a protected type:

In this example, instead of directly defining the protected object Obj , we first define a protected type P_Obj_Type and then declare Obj as an object of that protected type. Note that the main application hasn't changed: we still use Obj . Set and Obj . Get to access the protected object, just like in the original example.

Ada Programming/Tasking

  • 1.1 Rendezvous
  • 1.2 Selective Wait
  • 2 Protected types
  • 3 Entry families
  • 4 Termination
  • 6 Conditional entry calls
  • 7 Requeue statements
  • 8 Scheduling
  • 9 Interfaces and polymorphism
  • 10 Restrictions and Profiles
  • 11.1 Wikibook
  • 11.2.1 Ada 95
  • 11.2.2 Ada 2005
  • 12 Ada Quality and Style Guide

Tasks [ edit | edit source ]

A task unit is a program unit that is obeyed concurrently with the rest of an Ada program. The corresponding activity, a new locus of control, is called a task in Ada terminology, and is similar to a thread , for example in Java Threads . The execution of the main program is also a task, the anonymous environment task. A task unit has both a declaration and a body, which is mandatory. A task body may be compiled separately as a subunit, but a task may not be a library unit, nor may it be generic. Every task depends on a master , which is the immediately surrounding declarative region - a block, a subprogram, another task, or a package. The execution of a master does not complete until all its dependent tasks have terminated. The environment task is the master of all other tasks; it terminates only when all other tasks have terminated.

Task units are similar to packages in that a task declaration defines entities exported from the task, whereas its body contains local declarations and statements of the task.

A single task is declared as follows:

A task declaration can be simplified, if nothing is exported, thus:

It is possible to declare task types, thus allowing task units to be created dynamically, and incorporated in data structures:

Task types are limited , i.e. they are restricted in the same way as limited types, so assignment and comparison are not allowed.

Rendezvous [ edit | edit source ]

The only entities that a task may export are entries. An entry looks much like a procedure. It has an identifier and may have in , out or in out parameters. Ada supports communication from task to task by means of the entry call . Information passes between tasks through the actual parameters of the entry call. We can encapsulate data structures within tasks and operate on them by means of entry calls, in a way analogous to the use of packages for encapsulating variables. The main difference is that an entry is executed by the called task, not the calling task, which is suspended until the call completes. If the called task is not ready to service a call on an entry, the calling task waits in a (FIFO) queue associated with the entry. This interaction between calling task and called task is known as a rendezvous . The calling task requests rendezvous with a specific named task by calling one of its entries. A task accepts rendezvous with any caller of a specific entry by executing an accept statement for the entry. If no caller is waiting, it is held up. Thus entry call and accept statement behave symmetrically. (To be honest, optimized object code may reduce the number of context switches below the number implied by this poor description.)

There is, however, a big difference between a procedure and an entry. A procedure has exactly one body that is executed when called. There is no such relation between an entry and a corresponding accept statement. An entry may have more than one accept statement, and the code executed may be different each time. In fact, there even need not be an accept statement at all. (Calling such an entry leads to deadlock of the caller if not timed, of course.)

Ex. 2 The following task type implements a single-slot buffer, i.e. an encapsulated variable that can have values inserted and removed in strict alternation. Note that the buffer task has no need of state variables to implement the buffer protocol: the alternation of insertion and removal operations is directly enforced by the control structure in the body of Encapsulated_Buffer_Task_Type which is, as is typical, a loop .

Selective Wait [ edit | edit source ]

To avoid being held up when it could be doing productive work, a server task often needs the freedom to accept a call on any one of a number of alternative entries. It does this by means of the selective wait statement, which allows a task to wait for a call on any of two or more entries.

If only one of the alternatives in a selective wait statement has a pending entry call, then that one is accepted. If two or more alternatives have calls pending, the implementation is free to accept any one of them. For example, it could choose one at random. This introduces bounded non-determinism into the program. A sound Ada program should not depend on a particular method being used to choose between pending entry calls. (However, there are facilities to influence the method used, when that is necessary.)

creates two variables of type Encapsulated_Variable_Task_Type. They can be used thus:

Again, note that the control structure of the body ensures that an Encapsulated_Variable_Task_Type must be given an initial value by a first Store operation before any Fetch operation can be accepted.

Guards [ edit | edit source ]

Depending on circumstances, a server task may not be able to accept calls for some of the entries that have accept alternatives in a selective wait statement. The acceptance of any alternative can be made conditional by using a guard , which is Boolean precondition for acceptance. This makes it easy to write monitor-like server tasks, with no need for an explicit signaling mechanism, nor for mutual exclusion. An alternative with a True guard is said to be open . It is an error if no alternative is open when the selective wait statement is executed, and this raises the Program_Error exception.

Protected types [ edit | edit source ]

Tasks allow for encapsulation and safe usage of variable data without the need for any explicit mutual exclusion and signaling mechanisms. Ex. 4 shows how easy it is to write server tasks that safely manage locally-declared data on behalf of multiple clients. There is no need for mutual exclusion of access to the managed data, because it is never accessed concurrently . However, the overhead of creating a task merely to serve up some data may be excessive. For such applications, Ada 95 provides protected modules, based on the well-known computer science concept of a monitor . A protected module encapsulates a data structure and exports subprograms that operate on it under automatic mutual exclusion. It also provides automatic, implicit signaling of conditions between client tasks. Again, a protected module can be either a single protected object or a protected type, allowing many protected objects to be created.

A protected module can export only procedures, functions and entries, and its body may contain only the bodies of procedures, functions and entries. The protected data is declared after private in its specification, but is accessible only within the protected module's body. Protected procedures and entries may read and/or write its encapsulated data, and automatically exclude each other. Protected functions may only read the encapsulated data, so that multiple protected function calls can be concurrently executed in the same protected object, with complete safety; but protected procedure calls and entry calls exclude protected function calls, and vice versa. Exported entries and subprograms of a protected object are executed by its calling task, as a protected object has no independent locus of control. (To be honest, optimized object code may reduce the number of context switches below the number implied by this naive description.)

Similar to a task entry which optionally has a guard , a protected entry must have a barrier to control admission. This provides automatic signaling, and ensures that when a protected entry call is accepted, its barrier condition is True, so that a barrier provides a reliable precondition for the entry body. A barrier can statically be true, then the entry is always open.

Ex. 5 The following is a simple protected type analogous to the Encapsulated_Buffer task in Ex. 2.

Note how the barriers, using the state variable Empty, ensure that messages are alternately inserted and removed, and that no attempt can be made to take data from an empty buffer. All this is achieved without explicit signaling or mutual exclusion constructs, whether in the calling task or in the protected type itself.

The notation for calling a protected entry or procedure is exactly the same as that for calling a task entry. This makes it easy to replace one implementation of the abstract type by the other, the calling code being unaffected.

Ex. 6 The following task type implements Dijkstra's semaphore ADT, with FIFO scheduling of resumed processes. The algorithm will accept calls to both Wait and Signal, so long as the semaphore invariant would not be violated. When that circumstance approaches, calls to Wait are ignored for the time being.

This task could be used as follows:

Alternatively, semaphore functionality can be provided by a protected object, with major efficiency gains.

Ex. 7 The Initialize and Signal operations of this protected type are unconditional, so they are implemented as protected procedures, but the Wait operation must be guarded and is therefore implemented as an entry.

Unlike the task type above, this does not ensure that Initialize is called before Wait or Signal, and Count is given a default initial value instead. Restoring this defensive feature of the task version is left as an exercise for the reader.

Entry families [ edit | edit source ]

Sometimes we need a group of related entries. Entry families , indexed by a discrete type , meet this need.

Ex. 8 This task provides a pool of several buffers.

Note that the busy wait else null is necessary here to prevent the task from being suspended on some buffer when there was no call pending for it, because such suspension would delay serving requests for all the other buffers (perhaps indefinitely).

Termination [ edit | edit source ]

Server tasks often contain infinite loops to allow them to service an arbitrary number of calls in succession. But control cannot leave a task's master until the task terminates, so we need a way for a server to know when it should terminate. This is done by a terminate alternative in a selective wait.

The task terminates when:

  • at least one terminate alternative is open, and
  • there are no pending calls to its entries, and
  • all other tasks of the same master are in the same state (or already terminated), and
  • the task's master has completed (i.e. has run out of statements to execute).

Conditions (1) and (2) ensure that the task is in a fit state to stop. Conditions (3) and (4) ensure that stopping cannot have an adverse effect on the rest of the program, because no further calls that might change its state are possible.

Timeout [ edit | edit source ]

A task may need to avoid being held up by calling to a slow server. A timed entry call lets a client specify a maximum delay before achieving rendezvous, failing which the attempted entry call is withdrawn and an alternative sequence of statements is executed.

To time out the functionality provided by a task, two distinct entries are needed: one to pass in arguments, and one to collect the result. Timing out on rendezvous with the latter achieves the desired effect.

Symmetrically, a delay alternative in a selective wait statement allows a server task to withdraw an offer to accept calls after a maximum delay in achieving rendezvous with any client.

Conditional entry calls [ edit | edit source ]

An entry call can be made conditional, so that it is withdrawn if the rendezvous is not immediately achieved. This uses the select statement notation with an else part. Thus the constructs

seem to be conceptually equivalent. However, the attempt to start the rendezvous may take some time, especially if the callee is on another processor, so the delay 0.0; may expire although the callee would be able to accept the rendezvous, whereas the else construct is safe.

Requeue statements [ edit | edit source ]

A requeue statement allows an accept statement or entry body to be completed while redirecting it to a different or the same entry queue, even to one of another task. The called entry has to share the same parameter list or be parameter-less. The caller of the original entry is not aware of the requeue and the entry call continues although now to possibly another entry of another task.

The requeue statement should normally be used to quickly check some precondition for the work proper. If these are fulfilled, the work proper is delegated to another task, hence the caller should nearly immediately be requeued.

Thus requeuing may have an effect on timed entry calls. To be a bit more specific, say the timed entry call is to T1.E1, the requeue within T1.E1 to T2.E2:

Let Delta_T be the timeout of the timed entry call to T1.E1. There are now several possibilities:

1. Delta_T expires before T1.E1 is accepted.

2. Delta_T expires after T1.E1 is accepted.

Thus, although the original entry call may be postponed for a long time while T2.E2 is waiting to be accepted, the call is executing from the caller's point of view.

To avoid this behaviour, a call may be requeued with abort . This changes case 2 above:

2.a The call is requeued to T2.E2 before Delta_T expires.

2.b The call is requeued to T2.E2 after the expiration of Delta_T.

In short, for a requeue with abort, the entry call to T1.E1 is completed in cases 1, 2.a.1 and 2.b.1; it is aborted in 2.a.2 and 2.b.2.

So what is the difference between these three entries?

E1 has just been discussed. After the requeue, its enclosing task is free for other work, while the caller is still suspended until its call is completed or aborted.

E2 also delegates, however via an entry call. Thus E2 completes only with the completion of T2.E2.

E3 first frees the caller, then delegates to T2.E2, i.e. the entry call is completed with E3.

Scheduling [ edit | edit source ]

FIFO, priority, priority inversion avoidance, ... to be completed.

Interfaces and polymorphism [ edit | edit source ]

This language feature is only available from Ada 2005 on.

Tasks and protected types can also implement interfaces .

To allow delegation necessary to the polymorphism, the interface Printable shall be defined in its own package . It is then possible to define different task type implementing the Printable interface and use these implemetations polymorphically:

This feature is also called synchronized interfaces .

Restrictions and Profiles [ edit | edit source ]

Ada tasking has too many features for some applications. Therefore there exist restrictions and profiles for certain, mostly safety or security critical applications. Restrictions and profiles are defined via pragmas. A restriction forbids the use of certain features, for instance the restriction No_Abort_Statements forbids the use of the abort statement. A profile (do not confuse with parameter profiles for subprograms) combines a set of restrictions.

See 13.12: Pragma Restrictions and Pragma Profile [ Annotated ]

See also [ edit | edit source ]

Wikibook [ edit | edit source ].

  • Ada Programming
  • Ada Programming/Libraries/Ada.Storage IO
  • Ada Programming/Libraries/Ada.Task_Identification
  • Ada Programming/Libraries/Ada.Task_Attributes

Ada Reference Manual [ edit | edit source ]

Ada 95 [ edit | edit source ].

  • Section 9: Tasks and Synchronization [ Annotated ]

Ada 2005 [ edit | edit source ]

  • 3.9.4: Interface Types [ Annotated ]

Ada Quality and Style Guide [ edit | edit source ]

  • 4.1.9 Tasks
  • 4.1.10 Protected Types
  • Chapter 6: Concurrency

ada task entry

  • Ada Overview
  • More Topics of Interest
  • Features & Benefits
  • Ada Comparison Chart
  • Ada and Multicore
  • Case Studies

Ada Projects

  • Ada Standards
  • Free Tools & Libraries
  • Professional Tools and Services
  • Associations
  • Ada on the Web
  • Compilers and Conformity
  • Join the ARA
  • ARA Press Releases

Copyright © 2009-2023 Ada Resource Association Site Map | Contact Us

Copyright 1980, 1982, 1983 owned by the United States Government. Direct reproduction and usage requests to the Ada Information Clearinghouse .

COMMENTS

  1. What Is the Full Form of the Ada Programming Language?

    The Ada programming language is not an acronym and is named after Augusta Ada Lovelace. This modern programming language is designed for large systems, such as embedded systems, where reliability is important.

  2. What Are the ADA Kitchen Sink Requirements?

    According to ADA accessibility guidelines, ADA-compliant kitchen sinks must sit no higher than 34 inches above the floor and have shallow bowls between 5 and 6-1/2 inches deep. Sink drains are located at the rear instead of the middle.

  3. Where Can You Get a Complete List of ADA Procedure Codes?

    A complete list of American Dental Association, or ADA, procedure codes, known as Current Dental Terminology codes, are available on the CDT Code Check mobile application, states the ADA. This application is subscription-based and available...

  4. Tasking

    Task types . A task type is a generalization of a task. The declaration is similar to simple tasks: you replace task with

  5. Ada Programming/Tasking

    Ada supports communication from task to task by means of the entry call. Information passes between tasks through the actual parameters of the entry call.

  6. 6.1 Tasks, accept Statements and entry Calls

    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

  7. 9.5.2 Entries and Accept Statements

    Legality Rules. 13. An entry_declaration in a task declaration shall not contain a specification for an access parameter (see 3.10). 13.1

  8. Intro to Ada Pt. 5

    The first part defines the public interface to the task, specifying any entry calls. The second part contains the implementation of the task code. Ada tasks can

  9. 7: Tasks

    task type Worker is −− no entry end Worker ; task body Worker is ... end

  10. Ada Tasking: A Brief Review

    ▫ The task that accepts the entry call causes suspension of the calling

  11. Ada 83 LRM, Sec 9.5: Entries, Entry Calls, and Accept Statements

    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

  12. Ada Tutorial

    ada for an example of two tasks calling a third task's entry point. The task

  13. 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

  14. Entries and Accept Statements

    An entry_declaration in a task declaration shall not contain a specification for an access parameter (see 3.10). 13.a. Reason: Access parameters for task