TTK4145: Real Time Programming

Contributors
Mainly written by Mads Erdal, Erik Wilthil, Kristian Breistøl, Dag Slettebø, Andreas Lindahl Flåten, Anders Dahlen, Marius Thoresen, Eirik Lie Strandbråten, Bjørn Spockeli, Kristoffer Gryte, Kine Iversen, Cathrine Bruteig, Jostein Munz, Lars Skjærpe Midttun, Jon Håmann Brusevold and Geir Kulia.
Introduction to real-time systems
This chapter gives a definition of a real-time system, as well as characteristics and examples of real-time systems. This chapter will also present languages for programming and the development cycle for real-time systems. This book will consider three programming languages: Ada, Java and C, or more specifically: C/Real-Time POSIX, Real-Time Java and, well, Ada.
Definition of a real-time system
The Oxford Dictionary of Computing's definition of a real-time system: Any system in which the time at which output is produced is significant. This is usually because the input corresponds to some movement in the physical world, and the output has to relate to that same movement. The lag from input time to output time must be sufficiently small for acceptable timeliness 'Timeliness' is system-dependent. For instance, a missile guidance system requires output within milliseconds, whereas a computer-controlled water dam may only need output within a second.
- Young's (1982) definition of a real-time system:
- Any information processing activity or system which has to respond to externally generated input stimuli within a finite and specified period.
- Another definition is (Randell et al., 1995):
- A real-time system is a system that is required to react to stimuli from the environment (including the passage of physical time) within time intervals dictated by the environment.
The correctness of a real-time system depends not only on the logical result of the computation, but also on the time at which the result are produced. A hard real-time system are those where it is absolutely imperative that responses occur within the specified deadline. For soft systems, the system will still function correctly if deadlines are occasionally missed. A system may have both soft and hard real-time-sub-systems and soft and hard deadlines. A service may even have both a soft and a hard deadline with different reactions to the two deadlines.
A deadline that can be missed occasionally, but in which there is no benefit from late delivery, is called firm. Another means of classifying the role that time has in a real-time system, is to distinguish between reactive systems and time-aware systems. Time-aware systems make explicit references to the time frame of the enclosing environment. A reactive system is more concerned with relative times, however, since they are often also control systems, the need to be synchronized with their environment. In order for a reactive system to 'keep up with its environment' they are often structured to be time-triggered. All computations are periodic in that they have a defined cycle time, and are released for execution by an internal clock. The alternative is event- triggered where the environment explicitly controls the release for execution of some activity. These activities are termed aperiodic or sporadic if the number of releasing events within a time interval is bound.
Examples of real-time systems
A system where a computer interacts with the environment with sensors (for instance temperature or pressure transducer) and actuators (valves, motors in general) are examples of real-time systems. E.g plants, elevators, washing machines, etc. Real-time systems are used in process control, manufacturing, communication, multimedia, command and control, and pretty much everywhere else. These examples are thoroughly explained in the book for those interested.
Characteristics of real-time systems
Following are characteristics of real-time systems, which will be thoroughly examined in the later chapters of the book. A real-time system must, of course not exhibit all these characteristics, but should have facilities which support these characteristics.
Real-Time facilities
As mentioned, response time is crucial in any embedded (/real-time) system. As this is often near impossible to guarantee, due to computing resources under all possible conditions, real-time systems are often constructed using processors with considerable spare capacity. This is to ensure that a worst-case scenario does not produce unwelcome delays. Given adequate processing power, language and run-time support are required to enable the programmer to:
- specify times at which actions are to be performed;
- specify times at which actions are to be completed;
- support repeating (periodic or aperiodic) work;
- control (i.e. bound) the jitter on input and output operations;
- respond to situations where not all of the timing requirements can be met;
- respond to situations where the timing requirements are changed dynamically.
These are called real-time control facilities, and enable the program to synchronize with time itself.
Some systems are more demanding at certain periods, and therefore needs to change the timing requirements dynamically. As an example, an aircraft flight control system needs to focus at the task at hand, and also be ready to give all computing resources to handle an emergency. Such changes are generally known as mood changes, and the also have consequences for the temporal characteristics of the executing software.
Concurrent control of separate system components
Often, an embedded system must handle several coexisting external elements with which the computer must interact simultaneously. Then it is necessary to consider distributed and multiprocessor embedded systems. The problem then becomes how to express the concurrency in the structure of programs which exhibit concurrency. Chapter 4, 5 and 6 will consider various models of concurrent programming, and how to achieve reliable communication and synchronization between concurrent processes in the presence of design errors.
Low-level programming
An embedded system typically requires the computer components to interact with the external world, by monitoring sensors and controlling actuators for a wide variety of devices. These devices interface the computer via input and output registers, and their operational requirements are device- and computer-dependent, and may also generate interrupts. A real-time system is also often run by battery in a mobile device. We have also mentioned that real-time systems are very dependent on response time. Low-level programming eliminates both delay and extra power consumption by using built in language features. High-level programming enables the programmer to abstract away from implementation details, and to concentrate on solving the problem at hand. For a real-time programmer, this luxury is unacceptable, since delay and power consumption are two central problems when programming a real-time system.
Support for numerical computation
When a real-time system is to control some engineering activity, it often involves continuous signals. For very simple systems, one might have an analog controller working on the continuous signals. One wish to calculate an input, based on comparing the output with a reference signal. This will often require heavy calculations, depending on the complexity of the model and the number of inputs and outputs. Because of these difficulties, most controllers are implemented as digital computers. Since nature is analog, and computers are digital, converters are needed in order to convert analog signals to digital, and vice versa. This must again be taken into account when designing of the control algorithms. A fundamental requirement of a real-time programming language, therefore, is the ability to manipulate real or floating-point numbers. Fortunately most engineering languages do provide the necessary abstractions in this area.
Large and complex
Largeness is, by Lehman and Belady (1985) related to variety. Since most real-time systems respond to real-world events, the amount of variety is great. Hence, most real-time systems will exhibit the rather undesirable property of largeness. Due to this large- ness, real-time systems needs to be broken down into smaller components which can be managed effectively. Real-time languages provides features, such as abstract data types, classes, objects, generic components, separate compilation, etc to manage this software complexity.
Extremely reliable and safe
For some systems, failure is simply not an option. Systems providing life support, controlling nuclear plants or transferring money, can not fail, and if they do, however, fail, they must fail in a controlled way. When human interaction is required, care must be taken in the design of the interface in order to minimize the possibility of human error. Chapter 2 and 3 will consider the problems of producing reliable and safe software. Copying with both expected and unexpected error conditions will be examined in chapter 7 and 13(?).
Structure of the book
If you are interested in the structure of the book, read the book, not the compendium.
Development cycle for real-time systems
The most important stage in the development of any real-time system is the generation of a consistent design that satisfies an authoritative specification of requirements. The book does not focus on issues of design, but rather the investigation of language and operating system primitives, which allow designs to be realized. This section will give a brief overview of some of the typical stages that are passed through in the top-down design approach: - requirements specification - during which an authoritative specification of the system's required functional and meta-functional behaviour is produced; - architectural design - during which a top-level description of the proposed system is developed; - detailed design - during which the complete system design is specified; - coding - during which the system is implemented; - testing - during which the efficacy of the system is tested.
Requirement specification
Almost all computing projects start with an informal description of what is desired, followed by an extensive analysis of requirements. In this stage, functionality is defined, temporal behaviour made explicit and reliability requirements and the desired behaviour of the software in the event of component failure. The environment must also be modelled. Since interaction with the environment is especially important for a real-time system, maximum rate of interrupts, maximum number of objects and failure modes are all important.
Design activities
Decomposition and abstraction form the basis of most software engineering methods. Decomposition involves the systematic breakdown of the complex system into smaller and smaller parts, until components are isolated that can be understood and engineered by individuals or small groups.Abstraction enables the consideration of detail, particularly that appertaining to implementation, to be postponed. Sub-levels in the design must have well-defined roles in order to be verifiable. If the specification of the entire system can be verified just in terms of the specification of the immediate subcomponents, the decomposition is said to be compositional. This is an important property when formally analysing programs. Whenever a class (object) is used, typically, it involves the addition of some form of task (process). Both object and task abstractions are important in the design and implementation of reliable embedded systems. These forms of encapsulation lead to the use of modules. From the definition of modules, more sizable components can be defined that may even be re-usable in subsequent designs. Cohesion and coupling are two metrics that are often used to describe the relationships between entities within a design. Cohesion is concerned with how well a module holds together - its internal strength. Allworth and Zobel gave six measures of cohesion that range from the very poor (Coincidental) to the strong (Functional). Coupling, by comparison, is a measure of the interdependence of program modules. Two modules passing control information between the are said to be tight, or have high coupling. On the other end you have loose coupling if only data is communicated. A loose coupling means that the module is easy to replace with an other one, ie what we want. A good decomposition is one with strong cohesion, and loose coupling. This principle is equally true in sequential and concurrent programming domains.
Testing and simulation
To assist in any complex testing activity, a realistic test bed presents many attractions. For software, such a test environment is called a simulator. Using a simulator helps create normal, as well as abnormal system behaviour without having to meltdown a nuclear reactor in order to test the system. It is crucial that the simulator are able to reproduce accurately the sequence of events expected in the real system. As mentioned before, high reliability requirements are the essence of most real-time systems, as it is clear that testing must be extremely stringent.
Languages for programming real-time systems
There are three classes of programming languages to be identified; assembly languages, sequential systems implementation languages and high-level concurrent languages. These will be reviewed, after introducing some general language design criteria.
General language design criteria
Young (1982) lists the following six criteria as the basis of a real-time language design: security, readability, flexibility, simplicity, portability and efficiency.
- Security
- The security of a language design is a measure of the extent to which programming errors can be detected automatically by the compiler or language run-time support system. Some errors, like errors in the programmer's logic, cannot be detected automatically. A secure language must, therefore, be well structured and readable so that such errors can easily be spotted. The disadvantage of security is compiler complexity and compilation time (I'm not slacking of, my code is compiling!)
- Readability
- Readability depends on it having the appropriate choice of keywords, the ability to define types and the facilities for program modularization. Good readability leads to increased maintainability, security and reduced documentation costs. It does, however, tend to increase the length of any given program.
- Flexibility
- Flexibility is when the language allow the programmer to express all the required operations in a straightforward and coherent fashion, without having to report to operating system commands or machine code.
- Simplicity
-
Simplicity reduce cost associated with programmer training, minimizes the effort required to produce compilers and diminishes the possibility of making programming errors as a result of misinterpretation of the language. Simplicity and Flexibility can also be related to the expressive power, the ability to express the solution of a wide range of problems, and usability, the ease of use of the language.
- Portability
- Portability is the ability to run a program independent of the hardware on which it is executed on. Java claims to have this ability. This is difficult to achieve for a real-time system, however, it should be able to isolate the machine-dependent part of the program, from the machine-independent part.
- Efficiency
- Efficiency is the languages ability to allow efficient and predictable programs to be produced. Response times are crucial in a real-time system, however, this must be balanced against security, flexibility and readability requirements.
Assembly languages
Initially, most real-time systems were written in assembly language. Mostly due to the lack of support in high-level languages on microcomputers. Assembly language achieves efficient implementations, but is machine-oriented rather than problem-oriented. This keeps development costs high and makes maintenance difficult and expensive.
Sequential systems implementation languages
Most common programming languages used for real-time programming today, are sequential. They also tend to be weak in the facilities they provide for real-time control and reliability. As a result of these shortcomings, it is often necessary to rely on operating system support and assembly code inserts.
High-level concurrent programming languages
The software crisis in the 1970s had several symptoms. In short terms, the general language design criteria discussed above was seldom met. This was due to the fact that there were to many general-purpose languages in use. In the American Departement of Defence (DoD), there were over 450 different languages used in their embedded computer applications. DoD concluded that a single language was a desirable goal, and that no current language was suitable. So they decided to develop a new language, which resulted in Ada in 1983. Ada was later updated in 1995, 2005 and 2012, which is the current version.
Reliability and fault tolerance
Real-time and embedded systems usually have much higher requirements for reliability or safety. It is pretty annoying when Spotify crash or Matlab hangs, but these problems have little to no impact on the surroundings. A control system for, say avionics, have much higher demands on reliability.
Faults and failure
Let us first clear up some definitions
- Reliability
- A measure of the success with which the system conforms to some authorative specification of its behaviour.
- Fault
- A mechanical or algorithmic cause which, given the right conditions, produce an unexpected or incorrect result.
- Error
- An internal unexpected problem in a system, caused by activation of a fault.
- Failure
- System deviation from its specification.
Note that this implies a system maintain its reliability until a failure, and a failure only happens when the system does not interact with the environment in the prescribed manner. This means a reliable system either prevent activation of faults, or prevents the error from propagating through the system. This chain is shown in figure 1. Note that a failure in a subsystem will lead to a fault in a system. There are four sources of faults that can result in a system failure:
- Inadequate specification
- Software faults
- Hardware faults
- Interference in the communication subsystems
The last two are in some ways predictable errors in that the system designer will know what happens. For example, if a network module fails, a message will not be delivered. However, when an error occurs from incorrect interaction or wrong code, the result can be more unpredictable, as the scope of these effects can not be forseen as clearly as a hardware error. In this regard, a fault is the source of misbehaviour. Without regards to the above sources, there are three types of faults that can exist in a system. Also note that most system faults originate from incorrect or incomplete specification.
- Transient faults
- These faults occur at a particular time, remain in the system for a period, then disappear afterwards. An example of this is radioactivity.
- Permanent faults
- A fault that persists in the system until it is fixed. All software faults are permanent faults.
- Intermittent faults
- A special case of transient faults that occur repeatedly. Overheating is an example.
It is worth noting that software faults have a special name: The notorious bug. Bugs can be divided into two categories, with the names taken from their quantum physics counterparts. Bohrbugs are easily reproduced and easy to find and thus remedy. Heisenbugs are more complex bugs that arise from certain, often complex, combinations of events. A good example of this are threads that are not properly synchronized, and produce random behaviour subject to the state of each thread.
Failure modes and fault prevention
There are, generally speaking, two ways a system can fail. When a system produce a value failure, the output is wrong or even corrupted/unusable. When the system does not deliver the value in the right time slot, it has produced a time failure. A combination of these two are called an arbitrary failure. Note that while time failure intuitively covers delayed data, undelivered (delayed infinitely) and even information that is delivered too early are also considered time failure.
The book also give an extensive list of different ways a system might fail. These are given in a bullet list on page 32, and covers everything from complete chaos (uncontrollable failure) to an immune system (never fail) via the controlled shutdown/reduced operation failures. Fault prevention is an approach that aims to reduce the failures in a system by reducing the number of faults. The two main ways to do this is pretty intuitive: You can either find and remove the faults, or you can prevent their introduction in the first place. These are known as fault removal and fault avoidance. While there are hardware aspects to fault avoidance, such as using components that are suited for the task at hand, the most important is to write software with as few faults as possible. There are several ways to do this. Design methods, for example UML, and programming languages designed for real-time systems, such as Ada, will help reduce the number of faults. Extensive testing and simulation of the system will also help reduce the number of faults present during operation.
Fault tolerance and redundancy
Even though all precautions might have been taken during design, there will, in practice, still be faults in the system when it goes live. No software programmer writes 100% correct code all the time. No design specification is complete, consistent and unambiguous. Testing can only show the presence of faults, not their absence. Thus a system designer has to make sure the system continues to operate when a fault activates. This is known as fault tolerance, and curriculum describes three different levels.
- Full fault tolerance
- ensures the system continues operation as normal, at least for some time.
- Graceful degradation
- reduces the functionality to only perform core tasks while the system waits for repairs.
- Fail safe
- systems stops all operation, but ensures its integrity. For example, a failing pitch controller on a helicopter might reset the pitch to zero before shutting down.
In order to achieve this, system designers introduce extra elements to detect and handle faults. This is called redundancy, and is possible to implement in both software and hardware. We will focus on the software part, where N-version programming and recovery blocks are the most common techniques.
N-version programming
N-version programming relies on several pieces of software to compute the same result. There are several degrees of diversity, ranging from using different compilers for the same program, to let several teams build the same functionality in different programming languages. During execution, a driver process handles starts the programs, receive the result and act depending on the output. If the programs does not produce the same result, the driver might choose the most common result, ask the processes to compute it again, or simply terminate the faulty process. One of the downsides to this solution is that, in a real-time system, the driver process and the different programs might need to communicate between them, introducing a communication module as well. Add this to the fact that software is by far the most extensive and time consuming part of a real-time system, and the project costs might double by introducing a redundant language. There are also increased strains on hardware, or even need for separate hardware pieces, which increases the cost and/or complexity of the system. As a final note, recall that most software faults originate from the specification thus all N versions might suffer from the same fault.
Dynamic redundancy
Note that N-version programming is a case of static redundancy. All N versions of the program runs continuously no matter if the system fails or not. An alternative to this is to have modules in the background and only runs when an error is detected. This could decrease resource usage while still handling errors properly. This type of redundancy has four stages.
Error detection
Since the recovery system is not continually running, there needs to be some way to detect a fault. There are many triggers that could be used.
-
Replication checks are an extension to N-version programming that starts a recovery routine if one or more of the N results are wrong. Also consider a 2-version system: We cannot know which result is correct, but we know something is wrong, and action has to be taken.
-
Timing checks take the form of a watchdog timer that each process have to reset within a set interval. Note that timing checks can only be used to check for time failures Other error detection systems have to be used for value failures.
-
A reversal check can be used if there is a one-to-one relationship between the input and output. Thus, given an output, we can do a reversal calculation and compare it to the original input2.
-
Coding checks rely on extra information sent along with the data, which can be compared on the receiving side. This is often used in communication. For example, imagine an integer is being sent on the network. A checksum might be the square of the number, which is calculated on the sending side. When both numbers are received, the square root of the second number is compared to the first: A mismatch is proof of error.
-
Reasonableness checks rely on the knowledge of the system. This includes range checks, such as date and time validation.
-
Structural checks are used to ensure the data structures are correct. We know the number of items in a list; if this changes when it is not supposed to, something is wrong. Go users might recall that the json.Marshal-function produce an error when the information it has received does not fit the target struct.
-
Dynamic reasonableness checks the current output to the current output. Imagine a system on the form
$x' = ax$ suddenly change from a large positive value to a small negative value: This jump should not occur, and something is wrong.
Damage confinement
Normally, errors are not detected the instant it occurred. Thus when an error is detected, the system have to figure out how far it has spread. Damage confinement concerns how the rest of the system is affected by an error. The two main techniques of doing this are modular decomposition and atomic actions. Modular decomposition emphasizes on breaking down the system into components where each component only communicates with another through well-defined interfaces. The internal details are hidden from other modules. If done properly, all errors are detected before another module needs the result. Atomic actions will be described in a later chapter.
Error recovery
When the error is found and we know the extent of it, the recovery process can start. There are two methods of error recovery: Forward and backward recovery. Forward recovery involves continuing as before with selected corrections. While this is an efficent method of error recovery, it requires a good understanding of what the error is and how it can be remedied, else the corrections might only be for the worse. Backward recovery steps back to a previous state, called a recovery point, and tries to accomplish the same action with a different method. This works straightforward when there are no threads, but it quickly gets complicated with multiple threads, because of communication between threads: If we need to roll one thread back to an earlier recovery point, we might have to undo a message sent to the other thread. If this is the case, the other thread might have to roll back, requiring messages sent to other threads to be undone. Thus since one thread has had an error, multiple other threads will have to roll back as well. This is called the domino effect.
Fault treatment
While error corrections remedy the error, it does not remove the underlying fault. This fault might be easy to identify and/or remedy, and the experienced error will not happen again. Both hardware and software faults are relatively easy to fix once identified if the system is allowed to be brought oine. If the program have to be modified while executing, things get a little more complicated.
Recovery blocks
Recovery blocks are an implementation of dynamic recovery applied to software. It defines all entries to a software block as a recovery point, and then have an acceptance test before the block can exit. The method is summarized in Figure 2. Note that the acceptance test can involve several of the methods discussed for error detection. For a short summary of the difference between N-version programming and recovery blocks, read chapter 2.7.
Measuring reliability
Hardware reliability is pretty simple to measure: Take a bunch of similar components, test them, and establish a measure of reliability from the amount of components that fail. Software reliability, on the other hand, is much harder to measure as code only comes from one source, and it does not deteriorate over time. Thus new measurements of reliability have to be established. First, the notion that software was either working or not working was established. As discussed, software can be designed to maintain only critical functionality when shit hits the fan, and thus this boolean interpretation of software reliability is not accurate enough. A more recent definition of software reliability is based on probability. More specifically, it is determined as the probability that a given program will operate correctly in a specified environment for a specified length of time. One main way to calculate this is to use a growth model, increasing reliability for every fault that is corrected in the system. Another method is to calculate this probability statistically by applying a number of test cases to the system and, without correcting any fault, determine the amount of correct cases.
Exceptions and exception handling
There are five general requirements for an exception-handling facility: - As with all language features, the facility must be simple to understand and use.
-
The code for exception-handling should not be so obtrusive as to obscure understanding of the program's normal error-free operation. A mechanism which intermingles code for normal processing and exceptional processing will prove difficult to understand and maintain. It may well lead to a less reliable system.
-
The mechanism should be designed so that run-time overheads are incurred only when handling an exception. Although the majority of applications require that the performance of a program which uses exceptions is not adversely affected under normal operating conditions, this may not always be the case. Under some circumstances, in particular where speed of recovery is of prime importance, an application may be prepared to tolerate a little overhead on the normal error-free operation.
-
The mechanism should allow the uniform treatment of exceptions detected both by the environment and by the program. For example, an exception such as arith- metic overflow, which is detected by the hardware, should be handled in exactly the same manner as an exception raised by the program as a result of an assertion failure.
-
The exception mechanism should allow recovery actions to be programmed.
Exception handling in older real-time languages
Unusual return value is one of the simplest methods for exception handling, and works as follows:
if (function_call(parameters) == AN_ERROR) {
/* error handling code */
} else {
/* normal return code */
}
If a function already returns a value and it is not possible to partition the range of values to indicate an error, then status flags are used. These are atomic shared variables which can be set and tested. Using forced branch the instructions immediately following the subroutine call is skipped to indicate the presence (or absence) of an error. This is achieved by the subroutine incrementing its return address (program counter) by the length of a simple jump instruction to indicate an error-free (or error) return.
Modern exception handling
While most of the old real-time languages has error handling that changes the flow of execution in the program, most modern languages has exception handling built directly into the language. There are four classes of exceptions.
-
Detected by the environment and raised synchronously - an array bounds violation or divide by zero are examples of such exceptions.
-
Detected by the application and raised synchronously - for example, the failure of a program-defined assertion check.
-
Detected by the environment and raised asynchronously - an exception raised as the result of power failure or the failure of some health-monitoring mechanism.
-
Detected by the application and raised asynchronously - for example, one process may recognize that an error condition has occurred that will result in another process not meeting its deadline or not terminating correctly.
Chapter 3 mainly focus on synchronous exception handling. Ada requires exceptions to be declared as constants, while Java and C++ have a more object-oriented view.
Exception propagation
If an exception is raised it's not always a handler associated with that block that takes care of it. There are two possible methods for dealing with a situation where no immediate exception handler can be found. The first approach is to regard the absence of a handler as a programmer error which should be reported at compile-time. This can be challenging as it's not always possible for the compiler to check whether the calling context includes the appropriate exception handlers. The second approach is to look for handlers up the chain of invokers at run-time; this is called propagating the exception. This is the approach taken in Ada, Java and C++. If an exception is propagated outside its scope, it will be impossible to find a handler. Most languages have a 'catch all' exception handler to solve this potential problem.
- The resumption model
- Using this model the handler tries to cure the problem that caused the exception to be raised, and the invoker resumes as if nothing happened. This model is also called the notify model. This model is illustrated on p.67 in the textboook.
- The termination model
- Using this model, the control is not returned to the invoker after an exception is taken care of. This model is the most common, and is also called the escape model.
- The hybrid model
- With the hybrid model the handler decides whether the error is recoverable or not, and can choose if the invoker is to be terminated.
Exception handling in Ada, Java and C See textbook for examples.
Ada
Supports explicit exception declaration in the same fashion as constants, with keyword exception. Can be declared in the same place as any other declaration and has the same scope. Some standard exceptions, like Constraint_Error and Storage_Error has the whole program as scope. Every block in Ada can contain an optional collection of exception handlers declared at the end of the block. They have the following syntax: when (the name of the identity) => (action). see textbook for more details. when others can be used as the last exception-handling choice to pick up all exceptions not listed previously. In Ada an exception which is raised and not handled by a subprogram is propagated to the caller of the subprogram. Therefore, such an exception will only be handled by the initialization code if it itself called the subprogram. See textbook (p.73) for example. The exception can also be propagated by a program re-raising the exception in the 3 EXCEPTIONS AND EXCEPTION HANDLING 24 local handler. This facility is useful in the programming of last wishes. See textbook (p.74) for example. Exceptions will never meet requirement 3, as detecting possible error conditions will require some resources. The Ada language provides a facility that checks if standard exceptions raised by the run-time environment becomes too costly for a particular application. Difficulties with the Ada model of exceptions:
-
It can be hard to keep track of where an exception is raised.
-
Ada does not allow a full range of parameters to be passed to handlers only a character string. This can be inconvenient if an object of a particular type needs to be passed.
-
It is possible for exceptions to be propagated outside of the scope of their declaration. Such exceptions can only be trapped by others. However, they may go back into scope again when propagated further up the dynamic chain. This is disconcerting, although probably inevitable when using a block structured language and exception propagation.
Java
Java supports a termination model of exception handling, like Ada, but the exceptions are integrated into the object-oriented model. Raising an exception in Java is called throwing the exception, and it can only be han- dled from within a try-block. Each handler is specified using a catch statement, that works like a function declaration. A handler with parameter type T will catch a thrown object of type E if:
-
T and E are the same type; or
-
T is a parent (super) class of E at the throw point.
As with Ada, if no exception is found in the calling context of a function, the calling context is terminated and a handler is sought in a try-block within its calling context. Java supports a finally clause as part of a try-block. The code attached to this clause is guaranteed to execute whatever happens in the try-block irrespective of whether exceptions are thrown, caught or propagated, or, indeed, even if there are no exceptions thrown at all. To illustrate this, consider the following example, which will return false:
try {
return true;
} finally {
return false;
}
C
C does not define any exception-handling facilities within the language. This limits the possibilities of programming reliable systems, but it is possible to provide some form of exception-handling mechanism by using the macro facility of the language. To implement a termination model in C, it is necessary to save the status of a program's registers, and so on, on entry to an exception domain and then restore them if an exception occurs. This can be done using setjmp and longjmp.
Summary
Although many different models exist they all address the following issues:
-
Exception representation - an exception may, or may not, be explicitly represented in a language.
-
The domain of an exception handler - associated with each handler is a domain which specifies the region of computation during which, if an exception occurs, the handler will be activated. The domain is normally associated with a block, subprogram or a statement.
-
Exception propagation - this is closely related to the idea of an exception domain. It is possible that when an exception is raised there is no exception handler in the enclosing domain.
-
Resumption or termination model - this determines the action to be taken after an exception has been handled.
-
Parameter passing to the handler - this may or may not be allowed. The table below shows the exception-handling facilities of various languages. 
| Language | Domain | Propagation | Mode | Parameters |
| Java | Block | Yes | Termination | Limited |
| Java | Block | Yes | Termination | Yes |
| C++ | Block | Yes | Termination | Yes |
| CHILL | Statement | No | Termination | No |
| CLU | Statement | No | Termination | Yes |
| Mesa | Block | Yes | Hybrid | Yes |
It is not unanimously accepted that exception-handling facilities should be provided in a language. To sceptics, an exception is a goto where the destination is indeterminable and the source is unknown. They can, therefore, be considered to be the antithesis of structured programming.
Concurrent programming
Introduction
Definition: Concurrent programming is the name of techniques used to express potential parallelism and solving the resulting synchronization and communication challenges. Motivation behind concurrent programming:
-
Real-world systems (robots etc.) have a parallel nature.
-
Modern processors are much faster than the I/O-devices with whom they interact, and a sequential program that is waiting for I/O is unable to perform other operations.
-
To allow more than one processor to solve a problem.
-
Without concurrency, the software must be constructed as a single control loop. This structure cannot retain the logical distinction between system components. Speed-up of parallelizing: Amdahl's law gives that:
$\text{Max speed-up} = \frac{1}{(1-P)+\frac{P}{N}'}$ where P is the proportion of code that benefits from parallelization and N is the number of available processors.
Terms and definitions
Task/thread/process
A task/thread is the unit of parallelism and a single thread of control. A process is threads working within its own shared memory context.
Fundamental facilities
All concurrent programming consist of these fundamental facilities:
- Activities (threads, tasks or processes).
- Synchronization mechanisms.
- Support of communication between concurrent activities.
Types of interaction
Interaction between tasks can be grouped as one of the following: - Independent: No synchronization or communication between threads. - Cooperate: Synchronization and communication. - Competing: About their share of resources. Although they communicate and synch to obtain resources, they are essentially independent.
Hierarchy
Based on hierarchy and relation, one often distinguish between tasks to be:
-
Parent/child: Responsibility for the creation of another task. The parent may be delayed while the child is being created and initialized.
-
Guardian/dependant: A task may be dependent on the guardian task itself. The guardian is not allowed to terminate until all dependent tasks have terminated.
-
Sometimes the parent is also the guardian, but not necessarily with dynamic tasks.
Concurrent execution
Variation between the languages
The models of concurrency for the programming languages varies, especially when it comes to:
-
Structure: The number of tasks can be either:
- Static: fixed at compile-time
- Dynamic: tasks are created any time
-
The level of parallelism: Tasks can be either:
- Nested: allowed to be defined within other tasks
- Flat: not nested
-
Granularity (detaljnivå): Within languages that supports nested constructs we distinct between:
- Course grain parallelism: Program contains few tasks, each with a longer life. (Applies to most concurrent programming languages, typified by Ada.)
- Fine grain parallelism: Program contains a large number of simple tasks, some of which exist only for a single action (e.g Occam2).
-
Initialization: The tasks can be supplied with information by:
- Passing it as parameters (most modern languages allow this)
- Communicating explicitly after is has commenced its execution.
-
Termination: Can be done in a variety of ways:
- Normal completion of execution of the task's body
- Suicide, by a self-terminate statement
- Abortion, through action of another task
- An untrapped error condition
- Never (when executing non-terminating loops)
- When no longer needed
Task representation
The three basic mechanisms for representing concurrent execution is: fork and join, cobegin and explicit task declaration. The latter is most used in modern real-time languages.
Fork and join
The fork statement specifies that a designated routine should start executing concurrently with the invoker of the fork. The join statement allows the invoker to synchronize with the completion of the invoked routine. For example:
A procedure P(parent) begins and forks function F(child). They will now execute concurrently until the point when P states join. Here P will wait for F to finish (if it hasn't already done so). Fork and join allow for dynamic task creation and provide a means of passing information to the child via parameters. This can however be error prone in use, for example, in some systems a guardian must explicitly rejoin all dependants rather than merely wait for their completion.
Cobegin
The cobegin denotes the concurrent execution of a collection of statements, from
Explicit task declaration
The routines within the "Explicit task declaration" can state themselves whether they will be executed concurrently. This has become the standard way of expressing concurrency in real-time languages.
Language parallellism
Ada
The unit of parallelism is called task in Ada. Tasks consist of a specification and a body. They can be passed initialization data upon creation, but only discrete types and access (pointer) types. The tasks can be given as task types. Tasks that do not have types declared for them and have no pointers to them are called anonymous.
Dynamic tasks can be created by giving a non-static value to the bounds of an array (of tasks) or using the 'new' operator on an access type (of task type). Tasks created by the ('new') allocator have the important property that the guardian (called master in Ada) is not in the block in which it was created, but in the one that contains the declaration of the access type. That means that if task Y is created inside an inner block, but was first declared outside, the inner block can terminate even though Y hasn't yet terminated. Termination: A task will terminate as it completes its body, there is an unhandled exception, when no longer needed (done by a select statement, or when it is aborted. Any task can abort any other task, and as a result, all its dependants will also abort. This is a dangerous action, and should be used only when no other alternative actions are possible.
Java
Java has a predefined class, java.lang.Thread which provides threads. Java also has a interface, called Runnable, to express concurrent execution. This provide a run method.
There are two ways to create threads. One way is to declare a class to be a subclass of Thread and override the run method. The second way is to declare a class that implements the Runnable interface. Common for all threads in Java is that they are not created automatically when their associated objects are created, but must be explicitly created and started using the start method (e.g thread1.start();).
Java allows dynamic thread creation like Ada, but unlike Ada, Java allows arbitrary data to be passed as parameters. Java has no guardian concept, and relies on garbage collection to clean up objects no longer accessible. But the main function still terminates when all its threads are terminated. The join method works like described in "fork and join", and the isAlive method tells whether a target has terminated.
- Termination
- A thread terminates as it completes its body or there is an unhandled exception.
Deamon threads provide genereal service and typically never terminate until they are no longer needed (like in Ada using the select statement). In Java, one does not longer have the same opportunity to abort threads like in Ada.
C/Real-Time POSIX
C/Real-Time POSIX provides three mechanisms for creating concurrent activities. The first is the fork and wait mechanism. The second is spawn, which is a combination of fork and exec. It also allows for a process to contain several threads like in Ada and Java. A thread can be executed when it is called by pthread_create. Here the parameters are passed.
Termination: A thread can be terminated normally by returning from its start_routine or by calling pthread_exit. One thread can wait for another to terminate by the pthread_join function. The cleaning up and reclaiming the storage of a thread is called detaching.
Multiprocessor and distributed systems
Definitions:
- Multiprogramming
- Tasks multiplex their executions on a single processor
- Multiprocessing
- Tasks multiplex their executions on a multiprocessor system where there is access to shared memory
- Distributed Processing
- Tasks multiplex their executions on several processors which do not share memory
Global dispatching means that a task can be given to all available processors. From a real-time perspective, fixing tasks to certain processors usually will result in a more predictable response. Linux only allow threads to be constrained to execute on a limited set of processors. This is called processor affinity. Although Ada, Real-Time Java and C/Real-Time POSIX all support multiprocessor implementations, they do not provide mechanism to set the processor affinity of a task.
Steps to make a distributed system
Production of an application for a distributed system involves some steps not required when programs are produced for a single or multiprocessor platform.
- Partitioning
- dividing the system into parts
- Configuration
- the partitioned parts are associated with particular elements in the system
- Allocation
- turning the configured system into a system of executable modules
- Transparent execution
- the distributed software is executed so remote resources can be accessed independent of location
- Reconfiguration
- the dynamic change to the location of a software component
Language-supported vs operating-system-supported concurrency
There has been a long debate as to whether the support for concurrency should be provided by the language or the operating system. Pros and cons for the former follows: Pros:
- More readable and maintainable programs.
- There are many different operating systems.
- An embedded computer may not have any resident operating system.
Cons:
- Different languages have different models of concurrency.
- It may be difficult to implement a language's model of concurrency efficiently on top of an operating system's model.
- Operating systems API standards, such as POSIX, have emerged and therefore programs are more portable.