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 no-reply@cambridge.org
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.
This chapter is devoted to the introduction of annotations, procedures, recursion and repetitions, all concepts highly relevant to programming practice and programming methodology. In 2.1 we introduce Hoare triples as a specification method. Hoare triples are used in 2.2 for correctness proofs by annotation. In 2.3 and 2.4 we treat procedures in a programming language like Pascal. The specification and invocation rules are discussed in Section 2.3. The correctness of recursive procedures is treated in Section 2.4. The methods presented here are not new but deserve to be promoted.
In Section 2.5 we present and prove an abstract version of the rule for total correctness of recursive procedures. In 2.6 we introduce homomorphisms, functions from commands to predicate transformers that satisfy the standard laws of wp and wlp. Homomorphisms are used in 2.7 to give Hoare's Induction Rule for conditional correctness of recursive procedures, and a related rule for the necessity of preconditions. Finally, in Section 2.8, the results on recursive procedures are specialized to the repetition.
With respect to recursive procedures, this chapter is not ‘well-founded’. We only postulate some properties and proof rules, but the definition of the semantics of recursion (i.e., of the functions wp and wlp) and the proof of the postulates are postponed to Chapter 4.
Specification with Hoare triples
Weakest preconditions provide the easiest way to present predicate–transformation semantics. The formalism of Hoare triples, however, is completely equivalent and more convenient for program derivations and proofs of program correctness.
Recent years have seen considerable work on two approaches to belief revision: the so-called foundations and coherence approaches. The foundations approach supposes that a rational agent derives its beliefs from justifications or reasons for these beliefs: in particular, that the agent holds some belief if and only if it possesses a satisfactory reason for that belief. According to the foundations approach, beliefs change as the agent adopts or abandons reasons. The coherence approach, in contrast, maintains that pedigrees do not matter for rational beliefs, but that the agent instead holds some belief just as long as it logically coheres with the agent's other beliefs. More specifically, the coherence approach supposes that revisions conform to minimal change principles and conserve as many beliefs as possible as specific beliefs are added or removed. The artificial intelligence notion of reason maintenance system (Doyle, 1979) (also called “truth maintenance system”) has been viewed as exemplifying the foundations approach, as it explicitly computes sets of beliefs from sets of recorded reasons. The so-called AGM theory of Alchourrón, Gärdenfors and Makinson (1985; 1988) exemplifies the coherence approach with its formal postulates characterizing conservative belief revision.
Although philosophical work on the coherence approach influenced at least some of the work on the foundations approach (e.g., (Doyle, 1979) draws inspiration from (Quine, 1953; Quine and Ullian, 1978)), Harman (1986) and Gärdenfors (1990) view the two approaches as antithetical. Gärdenfors has presented perhaps the most direct argument for preferring the coherence approach to the foundations approach.
Since the beginning of artificial intelligence research on action, researchers have been concerned with reasoning about actions with preconditions and postconditions. Through the work of Moore (1980), Pratt's (1980) dynamic semantics soon established itself in artificial intelligence as the appropriate semantics for action. Mysteriously, however, actions with preconditions and postconditions were not given a proper treatment within the modal framework of dynamic logic. This paper offers such an analysis. Things are complicated by the need to deal at the same time with the notion of competence, or an actor's ability. Below, a logic of actions with preconditions and postconditions is given a sound and complete syntactic characterization, in a logical formalism in which it is possible to express actor competence, and the utility of this formalism is demonstrated in the generation and evaluation of plans.
The notion of actions with pre- and postconditions arose in artificial intelligence in the field of planning. In formulating a plan to reach some particular goal, there are a number of things which a planning agent must take into account. First, he will have to decide which actions can and may be undertaken in order to reach the goal. The physical, legal, financial and other constraints under which an actor must act will be lumped together below, since we will be interested in what is common to them all, namely that they restrict available options.
In the previous chapter we introduced the notion of a sequential process (agreeing that where no confusion would arise, the term process would be sufficient) in an informal way. Now we need to formalise this concept, and to provide some concrete examples of the representation of a process which will enable us to clarify the basic ideas behind systems of concurrent processes. We begin, however, by considering some of the issues concerning the nature of concurrent activity, and describing some of the notations which have been proposed whereby concurrency may be introduced into a program.
Specification of Concurrent Activity
There are a variety of ways of talking about concurrency, some of which have found their way into programming languages in various guises, and some of which are merely notational mechanisms for describing parallel activity.
When discussing the ways in which parallel activity can be specified within a programming language or system, it is useful to consider two orthogonal features of any proposed method. The first is the specification of where in a program a separate process may be started, and where, if at all, it terminates. The specification of where a new process may begin and end provides some synchronisation points within the concurrent program. We assume that it is possible to specify that two processes may execute in parallel, and it is at that point that the two processes are assumed to be synchronised.
Concurrency has been with us for a long time. The idea of different tasks being carried out at the same time, in order to achieve a particular end result more quickly, has been with us from time immemorial. Sometimes the tasks may be regarded as independent of one another. Two gardeners, one planting potatoes and the other cutting the lawn (provided the potatoes are not to be planted on the lawn!) will complete the two tasks in the time it takes to do just one of them. Sometimes the tasks are dependent upon each other, as in a team activity such as is found in a well-run hospital operating theatre. Here, each member of the team has to co-operate fully with the other members, but each member has his/her own well-defined task to carry out.
Concurrency has also been present in computers for almost as long as computers themselves have existed. Early on in the development of the electronic digital computer it was realised that there was an enormous discrepancy in the speeds of operation of electro-mechanical peripheral devices and the purely electronic central processing unit. The logical resolution of this discrepancy was to allow the peripheral device to operate independently of the central processor, making it feasible for the processor to make productive use of the time that the peripheral device is operating, rather than have to wait until a slow operation has been completed.
As long as all of the concurrent processes are proceeding completely independently of each other, we would expect them all to continue at their own speed until they terminate (if they do). If this does not happen, that is if the results of a process are affected by the presence or absence of another supposedly independent process, then we have to investigate the underlying mechanism to find the reason for this problem. For the purposes of the discussion of the concurrent processes themselves, they will have an effect on one another only if they are required to communicate with each other.
We have already seen one example of inter-process communication as manifested in one of the process creation and deletion methods. This entailed the parent process calling the fork operation to initiate the child process, and the subsequent join operation (if there is one) as a way of resynchronising the parent and child. These two operations are very restrictive, allowing as they do the parent and child to synchronise with each other only when the child begins and ends its execution. What is required is a more general mechanism (or mechanisms) by which two processes may communicate with each other, not just at the beginning and end of the lives of the processes. A discussion of several possible methods is the subject of this chapter.
For a number of years, Concurrent Programming was considered only to arise as a component in the study of Operating Systems. To some extent this attitude is understandable, in that matters concerning the scheduling of concurrent activity on a limited number (often one!) of processing units, and the detection/prevention of deadlock, are still regarded as the responsibility of an operating system. Historically, it is also the case that concurrent activity within a computing system was provided exclusively by the operating system for its own purposes, such as supporting multiple concurrent users.
It has become clear in recent years, however, that concurrent programming is a subject of study in its own right, primarily because it is now recognised that the use of parallelism can be beneficial as much to the applications programmer as it is to the systems programmer. It is also now clear that the principles governing the design, and the techniques employed in the implementation of concurrent programs belong more to the study of programming than to the management of the resources of a computer system.
This book is based on a course of lectures given over a number of years initially to third, and more recently to second year undergraduates in Computing Science. True to the origins of the subject, the course began as the first part of a course in operating systems, but was later separated off and has now become part of a course in advanced programming techniques.
We introduce here a simple concurrency kernel, showing how it may be used to provide pseudo-concurrency on a single processor, and also in the chapter we shall show how some of the concurrency constructs introduced in chapters 4 and 5 may be implemented.
We assume that the machine on which the kernel is to be implemented consists of a single processor of the conventional type with a single monolithic address space. A simple Motorola M68000 based system would be an appropriate type of system on which to run the kernel. To make the kernel more useful, it would be possible to consider hardware which provides a virtual address translation capability, in which case the various processes could be provided with distinct address spaces in which to operate. This, however, not only makes the kernel more complicated, but also makes sharing of memory more difficult. For the purposes of the simple kernel which we are considering here, we shall assume a single (shared) address space.
The implementation language will be Pascal, and for simplicity we shall assume that a hypothetical Pascal machine has been implemented on top of the basic hardware. Apart from the ability to run Pascal programs, we shall also require some additional features to support the concurrency kernel to be constructed. It is clear that much of the underlying support, both for Pascal itself and for the concurrency kernel, will have to be provided through a small amount of assembly code.
Before embarking upon a discussion of the concurrency facilities provided by various programming languages, it is necessary to issue an apology and a warning. The set of languages discussed in this chapter is intended to be a representative selection of languages available at the present time, which between them illustrate a number of different approaches to concurrency. It is hoped that no reader will feel offended at not finding his favourite concurrent language in the discussion. Examples are chosen to demonstrate a variety of treatments of controlled access to shared data, usually in the form of a shared data structure with limited access to the operations on the data structure (i.e. variations on the ‘monitor’ theme), and also languages supporting some form of message passing or mailbox handling are considered.
It should be clear that beneath each of the mechanisms to be found in any of the languages discussed in this chapter, some lower level code (or hardware) must be supplied to provide, if not the reality, then the illusion of concurrency. Beneath any successful concurrent language there is a system which manages the concurrency, schedules processes onto processors, provides the fundamental operations to implement process creation and deletion, and to provide communication facilities.
When any new language is designed, the designer attempts to correct many of the inadequacies (as he sees them) of one or more existing languages.
In the previous chapter we considered the basic problem of communication between processes, and reduced it to the problem of preventing two processes from accessing the same shared resources simultaneously. It should be clear that from such mechanisms it is possible to construct a large number of different methods for communication. It is also clear that the relationship between the lowlevel mechanisms described in the previous chapter and the higherlevel constructs is similar to the relationship between a low-level assembly language program and the corresponding program in a highlevel language; the first provides a basic mechanism, which clearly is sufficient, if inconvenient, to implement all the necessary synchronisation. The second expresses the solution to the problem in a much more “problem oriented” fashion.
In this chapter we shall consider a number of high-level constructs which have been proposed which allow a more structured approach to many inter-process communication/synchronisation problems. In the same way that the use of high-level languages allows the programmer to express his algorithms in a more natural way, and hence will make the inclusion of simple logical errors less likely, so the use of high-level synchronisation constructs in concurrent programs will also tend to reduce the incidence of elementary concurrent programming errors.
To take a small example, consider program 4.1, which uses binary semaphores to provide an implementation of the critical section problem (c.f. program 3.12).
All the high-level constructs discussed in the previous chapter have been of the “shared data” form. By this we mean that processes wishing to communicate with each other do so not by addressing one another directly, but by accessing data which is known and available to them all. The constructs also provide ways of accessing the shared data which ensure that the data itself is not compromised by undesirable simultaneous accesses by competing processes by offering operations in the form of procedures and functions to control the manipulation of the data. The data structure itself is purely passive, and changes are made to the structure by allowing procedures and functions to be called by the active elements in the system, namely the processes. As these procedures are called, they in a sense become part of the process which calls them, and the data itself temporarily becomes part of the address space of the calling process. We could also use the term “procedure oriented” to describe this view. We observe that the competing processes are not required to have any knowledge of the identities of their competitors, but merely to know the name of the object(s) they wish to access, and which operation(s) they wish to apply to the data.
By contrast, we shall now discuss a different class of techniques for allowing concurrent processes to communicate with each other.
In this last chapter we shall survey a number of instances where infinite electrical networks are useful models of physical phenomena, or serve as analogs in some other mathematical disciplines, or are realizations of certain abstract entities. We shall simply describe those applications without presenting a detailed exposition. To do the latter would carry us too far afield into quite a variety of subjects. However, we do provide references to the literature wherein the described applications can be examined more closely.
Several examples are presented in Sections 8.1 and 8.2 that demonstrate how the theory of infinite electrical networks is helpful for finding numerical solutions of some partial differential equations when the phenomenon being studied extends over an infinite region. The basic analytical tool is an operator version of Norton's representation, which is appropriate for an infinite grid that is being observed along a boundary. In effect, the infinite grid is replaced by a set of terminating resistors and possibly equivalent sources connected to the boundary nodes. In this way, the infinite domain of the original problem can be reduced to a finite one – at least so far as one of the spatial dimensions is concerned. This can save computer time and memory-storage requirements.
In Section 8.3 we describe two classical problems in the theory of random walks on infinite graphs and state how infinite-electrical-network theory solves those problems. Indeed, resistive networks are analogs for random walks.