We use cookies to distinguish you from other users and to provide you with a better experience on our websites. Close this message to accept cookies or find out how to manage your cookie settings.
To save content items to your account,
please confirm that you agree to abide by our usage policies.
If this is the first time you use this feature, you will be asked to authorise Cambridge Core to connect with your account.
Find out more about saving content to .
To save content items to your Kindle, first ensure [email protected]
is added to your Approved Personal Document E-mail List under your Personal Document Settings
on the Manage Your Content and Devices page of your Amazon account. Then enter the ‘name’ part
of your Kindle email address below.
Find out more about saving to your Kindle.
Note you can select to save to either the @free.kindle.com or @kindle.com variations.
‘@free.kindle.com’ emails are free but can only be saved to your device when it is connected to wi-fi.
‘@kindle.com’ emails can be delivered even when you are not connected to wi-fi, but note that service fees apply.
The problem of sharing resources between processes was briefly discussed in Chapter 3. Two requirements were identified as being essential: mutual exclusion and condition synchronisation. This chapter discusses various ways in which these requirements can be met in Ada without having to encapsulate the resource in a server task and without having to use the rendezvous. Ada gives direct support to protected data by the protected object feature, the discussion of which is the main focus of this chapter. However, the language does also support the notions of atomic and volatile data, which are covered in Section 7.13.
Protected objects
A protected object in Ada encapsulates data items and allows access to them only via protected subprograms or protected entries. The language guarantees that these subprograms and entries will be executed in a manner that ensures that the data is updated under mutual exclusion. Consequently, they are rather like monitors found in previous concurrent programming languages (as described in Chapter 3).
A protected unit may be declared as a type or as a single instance; it has a specification and a body.
The development of the Ada programming language forms a unique and, at times, intriguing contribution to the history of computer languages. As all users of Ada must know, the original language design was a result of competition between a number of organisations, each of which attempted to give a complete language definition in response to a series of documented requirements. This gave rise to Ada 83. Following 10 years of use, Ada was subject to a complete overhaul. The resulting language, Ada 95, had a number of significant changes from its predecessor. A further 10 years of use has produced another version of Ada, known as Ada 2005, this time the changes are less pronounced and yet there are some key extra facilities, especially in the areas of real-time programming.
Closely linked to the development of Ada has been this book on its concurrent features. Starting out as ‘Concurrent Programming in Ada’, it became ‘Concurrency in Ada’ when the Ada 95 version of the language was defined. There were two editions of this title. With the new features of Ada 2005, it has been decided to broaden the focus of the book to include real-time issues – hence this first edition of the new title ‘Concurrent and Real-Time Programming in Ada 2005’. No prior knowledge of concurrent programming (in general) or of Ada tasking (in particular) is assumed in this book. However, readers should have a good understanding of at least one high-level sequential programming language and some knowledge of operating system principles.
This book is aimed both at professional software engineers and at students of computer science (and other related disciplines).
The Ada language addresses many aspects of software engineering, but it is beyond the scope of this book to discuss in detail its support for such topics as programming in the large, reuse and so on. Rather, the goal of this book is to discuss, in depth, the Ada model of concurrency and how it is affected by other areas of the language, for example exception handling. This chapter explores the interaction between the object-oriented programming (OOP) facilities and tasking.
Ada 83 was defined long before object-oriented programming became popular, and it is a credit to the Ada 95 language designers that they managed to introduce OOP facilities without having to alter the basic structure of an Ada program. However, the Ada 95 model had several limitations. In particular, it did not support the standard prefix notation (also called the distinguised receiver syntax), Object_Name.Method_Name(Params), found in most OOP languages. Instead, it required the object name to be given as part of the parameter list of the method. Although this had some advantages, it caused some confusion when programmers moved from languages like C++ and Java to Ada. Ada 2005 now allows the standard OOP notation with the object name prefixing the subprogram name when it is called.
Ada 95 also didn't attempt to integrate the language's support for concurrent programming directly into the OOP model. Instead, the models were orthogonal and paradigms had to be created to allow the benefits of OOP to be available in a concurrent environment.
Ada is a high-level programming language; it provides abstract constructs that allow programs to be constructed easily and safely. However, it is recognised that one of the intended application areas for Ada is the production of embedded systems. Often these (and other) systems require the programmer to become more concerned with the implementation, and efficient manipulation, of these abstract program entities. Ada resolves this conflict in two ways:
by allowing the programmer to specify the representation of program abstractions on the underlying hardware, for example by specifying the layout of a record or the address of a variable; and
by having extra facilities in the Systems Programming Annex for interrupt handling, controlling access to shared variables, unique identification of tasks, task attributes and the notification of task termination. As with all Ada annexes, these features need not be supported by all compilers.
The areas that are of concern in this book are those which relate directly to the tasking model. These are:
device driving and interrupt handling – covered in this chapter;
access to shared variables – previously covered in Section 7.12
task identification – motivated in Section 4.4 and covered fully in this chapter;
task attributes – covered in this chapter;
task termination (notification thereof) – covered in Section 15.7.
Other relevant embedded systems issues such as access to intrinsic subprograms, control over storage pools and data streams, and the use of machine code inserts are not dealt with.
Any language, natural or computer, has the dual property of enabling expression whilst at the same time limiting the framework within which that expressive power may be applied. If a language does not support a particular notion or concept, then those that use the language cannot apply that notion and may even be totally unaware of its existence. Pascal, C, C++, Eiffel, Fortran and COBOL share the common property of being sequential languages. Programs written in these languages have a single thread of control. They start executing in some state and then proceed, by executing one statement at a time, until the program terminates. The path through the program may differ due to variations in input data, but for any particular execution of the program there is only one path.
A modern computer system will, by comparison, consist of one or more central processors and many I/O devices, all of which are operating in parallel. Moreover, an operating system that provides for interactive use will always support many executing programs (called processes), in the sense that they are being timesliced onto the available processors. The effect of fast process switching is to give behaviour that is almost indistinguishable from true parallelism. In the programming of embedded systems, one must deal with the inherent parallelism of the larger system. A real-time language must therefore provide some facility for multi-programming. This can be achieved by specifying a standard interface to a multiprocessing operating system or by allowing multiple process systems to be expressed in the language itself.
Ada provides for the direct programming of parallel activities.
This chapter introduces a number of language features that are new in Ada 2005. The main focus is on execution time and how tasks can monitor and control the amount of processor time they are using. For real-time systems this is of crucial importance as the processor is usually the resource in least supply. It needs to be
used in a manner that is sympathetic to the scheduling policy of the system,
not over-used by failing components (tasks), but
fairly reallocated dynamically if spare capacity becomes available.
However before considering these topics, a more general view of Ada's model of event handling is warranted. The facilities for execution-time control all make use of events and event handling, and hence this chapter will start by examining events and a particular kind of event termed a timing event.
Events and event handling
It is useful in concurrent systems to distinguish between two forms of computation that occur at run-time: tasks and events. A task (or process or thread) is a long-lived entity with state and periods of activity and inactivity. While active, it competes with other tasks for the available resources – the rules of this competition are captured in a scheduling or dispatching policy (for example, fixed priority or EDF). By comparison, an event is a short-lived, stateless, one-shot computation. Its handler's execution is, at least conceptually, immediate; and having completed it has no lasting direct effect other than by means of changes it has made to the permanent state of the system. When an event occurs we say it is triggered; other terms used are fired, invoked and delivered.
Computer languages, like natural languages, evolve. For regulated languages, changes are consolidated into distinct versions at well-defined points in time. For Ada, the main versions have been Ada 83, Ada 95 and now Ada 2005. Whereas Ada 95 was a significantly different language from its ancestor, Ada 2005 is more an upgrade. It brings Ada up to date with respect to current practice in other languages, operating systems and theory – especially in the real-time domain.
Although Ada is a general purpose programming language, much of its development has been driven by the requirements of particular application areas. Specifically, the needs of high-integrity and safety-critical systems, real-time systems, embedded systems and large complex long-life systems. To support this wide range of applications, Ada has a large number of language features and primitives that can be grouped into the following:
strong typing with safe pointer operations,
object-oriented programming support via tagged types and interfaces,
hierarchical libraries and separate compilation,
exception handling,
annexes to give support to particular application domains,
low-level programming features that enable device drivers and interrupt handlers to be written,
an expressive concurrency model and
an extensive collection of entities that support real-time systems programming.
This book has concentrated on the last three items in this list to provide a comprehensive description of real-time and concurrent programming. These are two of the unquestionable strengths of the Ada language.
The models of synchronisation discussed in the previous four chapters have the common feature that they are based on avoidance synchronisation. Guards or barriers are used to prevent rendezvous and task-protected object interactions when the conditions are not appropriate for the communications event to start. Indeed, one of the key features of the tasking model is the consistent use of avoidance to control synchronisation. The use of guards and barriers represents a high-level abstract means of expressing and enforcing necessary synchronisations; and as such they can be compared favourably with the use of low-level primitives such as semaphores or monitor signals (see Chapter 3). This chapter starts by giving a more systematic assessment of avoidance synchronisation in order to motivate the requirement for ‘requeue’. It then describes the syntax and semantics of the requeue statement and gives examples of its use.
The need for requeue
Different language features are often compared in terms of their expressive power and ease of use (usability). Expressive power is the more objective criterion, and is concerned with the ability of language features to allow application requirements to be programmed directly. Ease of use is more subjective, and includes the ease with which the features under investigation interact with each other and with other language primitives.
In her evaluation of synchronisation primitives, Bloom (1979) used the following criteria to evaluate and compare the expressive power and usability of different language models.
The last few chapters have demonstrated the extensive set of facilities that Ada 2005 provides for the support of real-time programming. The expressive power of the full language is clearly more comprehensive than any other mainstream engineering language. There are, however, situations in which a restricted set of features are desirable. This chapter looks at ways in which certain restrictions can be identified in an Ada program. It then describes in detail the Ravenscar profile, which is a collection of restrictions aimed at applications that require very efficient implementations, or have high-integrity requirements, or both. This chapter also includes a discussion of the metrics identified in the Real-Time Systems Annex.
Restricted tasking and other language features
Where it is necessary to produce very efficient programs, it is useful to have runtime systems (kernels) that are tailored to the particular needs of the program actually executing. As this would be impossible to do in general, the Ada language defines a set of restrictions that a run-time system should recognise and ‘reward’ by giving more effective support. The following restrictions are identified by the pragma called Restrictions, and are checked and enforced before run-time. Note however, that there is no requirement on the run-time to tailor itself to the restrictions specified.
The major difficulties associated with concurrent programming arise from process interaction. Rarely are processes as independent of one another as they were in the simple example of the previous chapter. One of the main objectives of embedded systems design is to specify those activities that should be represented as processes (that is, active entities and servers), and those that are more accurately represented as protected entities (that is, resources). It is also critically important to indicate the nature of the interfaces between these concurrent objects. This chapter reviews several historically significant inter-process communication primitives: shared variables, semaphores, monitors and message passing. Before considering language primitives, however, it is necessary to discuss the inherent properties of inter-process communication. This discussion will be structured using the following headings:
Data communication;
Synchronisation;
Deadlocks and indefinite postponements;
System performance, correctness and reliability.
These are the themes that have influenced the design of the Ada tasking model.
As this model directly represents active and protected entities, there are two main forms of communication between active tasks:
direct – task-to-task communication;
indirect – communication via a protected resource.
Both these models are appropriate in Ada programs. In the following sections, however, we start by considering the problems of communicating indirectly by the use of only passive entities.
Data communication
The partitioning of a system into tasks invariably leads to the requirement that these tasks exchange data in order for the system to function correctly. For example, a device driver (a process with sole control over an external device) needs to receive requests from other processes and return data if appropriate.