Abstract
We examine the interaction of abstraction, distribution, and synchronization in determining the granularity of modules in object-based concurrent systems. The relation between linearizability and serializability as correctness criteria for processes with internal concurrency is explored.
Module granularity in object-based programming languages depends on the following factors:
the abstraction boundary (unit of encapsulation) the information-hiding interface a module presents to its users or clients
the distribution boundary (unit of name space) the boundary of the name space accessible from within a module
the synchronization boundary (unit of concurrency) the boundary at which threads entering a module synchronize with ongoing activities in the module
The abstraction boundary specifies the unit of encapsulation and is the fundamental determinant of object granularity. It determines the interface between an object and its clients and the form in which resources provided by an object to its clients may be invoked.
The distribution boundary is the boundary of accessible names visible from within an object. The abstraction boundary is encountered by a user of a module looking inward while the distribution boundary is encountered by an agent within a module looking outward. The distribution boundary may be coarser than the abstraction boundary, as in block-structure languages, or finer, when a large abstraction (say in airline reservation system) is implemented by distributed components. When the abstraction and distribution boundaries coincide, then modules can communicate only by message passing. We say a module is distributed if its distribution boundary coincides with its abstraction boundary.
Distribution is defined independently of the notion of concurrency in terms of inaccessibility of nonlocal names. Concurrency may likewise be defined independently of distribution in terms of threads and thread synchronization. Threads may in general interact in an undisciplined fashion in a single large namespace with no notion of abstraction or distribution. Object-based programming requires a disciplined use of threads for objects with a well-defined notion of abstraction and distribution.
Concurrent objects (processes) require incoming threads associated with messages or remote procedure calls to synchronize with ongoing activities within a process. If synchronization can occur only at the abstraction boundary of processes, then processes are internally sequential and our model is one of communicating sequential processes. In this case, the unit of abstraction coincides with the unit of concurrency.
Communicating sequential processes whose name space boundary coincides with their abstraction boundary are called distributed sequential processes. Distributed sequential processes have the same interface for abstraction, distribution, and concurrency, as illustrated in Figure 1. Distributed sequential processes are attractively simple, both conceptually and in terms of their implementation. However, their expressive power may be inadequate for certain applications. Abstractions that encapsulate a large data structure, such as the database of an airline reservation system, may require fine-grained concurrency within an abstraction both to model the application accurately and for purposes of efficiency. Conversely, a network of sequential computers with multiple objects at each node is naturally modeled by a unit of concurrency coarser than its unit of abstraction.
In order to understand design alternatives for internal process concurrency we distinguish between sequential processes with a single thread, quasi-concurrent processes with at most one active thread, and concurrent processes with multiple threads, as illustrated in Figure 2.
sequential process: A process that has just one thread.
quasi-concurrent process: A process that has at most one active thread.
concurrent process: A process that may have multiple active threads.
Languages supporting sequential processes include Ada [DOD], CSP [Ho1], and Nil [SY]. Messages to perform operations are queued at the process interface until the already executing process is ready to accept them. Rendezvous synchronization joins an incoming thread with the active thread for purposes of execution and then separates the threads so that invoking and invoked processes may again proceed in parallel.
Languages supporting quasi-concurrent processes include ABCL/1 [YBS] and Concurrent Smalltalk [YT]. Monitors [Ho, Ha] are quasi-concurrent processes which allow threads to be suspended while waiting for a condition to be fulfilled and to be resumed when the condition is satisfied. Monitors differ from sequential processes in having internal “condition queues” of suspended threads as well as entry queues of threads at the interface. A waiting thread can become active only if the currently executing thread terminates or is suspended. Suspended threads usually have priority over incoming threads so that incoming threads can execute only if there is no suspended thread ready to resume execution. The process is the synchronization boundary for incoming threads, but suspended threads are synchronized at a finer level of granularity, namely at the statement level.
Argus [LS], Clouds [LB], and Actor languages [Ag] have fine-grained concurrent processes with multiple threads. Concurrent processes do not require incoming threads to be synchronized at the process boundary. Synchronization can be delayed till a thread attempts to access shared data in critical regions (atomic objects in Argus). At this time the thread may be suspended until the shared data can safely be accessed. Concurrent processes support fine-grained concurrency control that permits synchronization to be delayed from process entry time to the time of entry to critical regions.
Both quasi-concurrent and fully-concurrent processes require fine-grained synchronization within the abstraction boundary. Quasi-concurrent processes support fine-grained synchronization at the statement level by means of conditional wait statements that automatically resume when their condition becomes true. Concurrent processes must perform both synchronization and data protection within processes and may use locking to protect data within a process from concurrent access by multiple threads [LS].
Sequential and quasi-concurrent processes do not need to protect local data from concurrent access since at most one thread can be active within a process. However, quasi-concurrent processes may leave data in an unstable state when a process is suspended, and may require transaction protocols [BG] to ensure correctness. Languages like ABCL/1 and Concurrent Smalltalk, and monitor languages which support quasi-concurrent processes without transactions, cannot protect local data from being modified while a thread executing a transaction on that data is suspended.
Correctness for concurrent processes may be defined in terms of correctness for sequential processes in terms of linearizability [HW]. A computation within a concurrent process is said to be linearizable if it is equivalent to a sequential computation in which methods are executed in an instantaneous non-overlapping order within their interval of invocation and response. A linearized computation for a typed process is acceptable if it is consistent with the requirements (axioms) of the data type. For example, a computation within a concurrent queue is acceptable if it can be linearized so that queue elements are enqueued before they are dequeued and dequeued in a first-in-first-out order.
Linearizability and acceptability are locally definable. A computation on a collection of concurrent objects is acceptable if there is an acceptable linearization for each of the objects in the collection. A program is correct if all its potential computations are acceptable.
Linearizability allows correctness to be specified for each type in terms of its sequential properties (axioms). But it does not allow dependency relations between objects of the kind that arise in transactions to be specified, such as the dependence of two account objects between which funds are transferred.
Correctness of concurrently executing transactions may be defined by serializability, namely by the requirement that computations with concurrently executing transactions are equivalent to some serial order of execution of the transactions. Serializability flattens a collection of concurrently executing transactions into some serial order of execution. This is a much more radical restructuring of the computation than linearizability which freely allows concurrency between objects since this has no impact on local correctness.
Linearizability and serializability have in common the idea of reasoning about concurrent computations by postulating their equivalence to some sequential computation. Linearizability is less ambitious in requiring only sequential equivalence for concurrently executing methods of individual objects and is able to capture type correctness. Serializability aims to capture exclusive access to shared resources by sequences of methods and requires a much stronger form of sequential equivalence that treats sequences of methods rather than individual methods as the units to be serialized.
Objects and transactions are two complementary structuring mechanisms for concurrent systems. Objects partition the system state into encapsulated components with characteristic behavior determined by sets of applicable operations. Transactions partition system actions into sequences with exclusive access to shared objects during the complete duration of their execution. Object-based transaction systems support structuring of applications in terms of both objects and transactions.
Concurrent object-based languages such as ABCL/1 and Concurrent Smalltalk do not contain a notion of transactions while transaction systems are generally not object-based. In order to develop object-based transaction systems, both the notion of object-based encapsulation and of transaction-based atomicity must be supported in a single system. The complementary concepts of linearizability and serializability reduce reasoning about concurrent systems to reasoning about equivalent sequential systems.
Modularity in object-based transaction systems differs from the traditional object-based notion of modularity. It encompasses temporal modularity associated with atomic actions as well as state-based modularity. Module granularity in such systems is determined by temporal boundaries of atomic actions as well as by spatial boundaries associated with abstraction, distribution, and synchronization. The space-time interaction of spatial and temporal module granularity is the subject of current research.