[IEEE Comput. Soc Sixth IEEE International Conference on Engineering of Complex Computer Systems -...

10
Concurrent Programming Made Easy Rafael Ramirez Andrew E. Santosa Roland H. C. Yap School of Computing National University of Singapore (rafael,andrews, tyap} @comp.nus. edu.sg Abstract The task of programming concurrent systems is substan- tially more dificult than the task of programming sequen- tial systems with respect to both correctness and eflciency. In this paper we describe a constraint-based methodology for writing concurrent applications. A system is modeled as: (a) a set of processes containing a sequence of “mark- ers” denoting the processes points of interest; and (b) a constraint store. Process synchronization is specijied by in- crementally adding constraints on the markers’ execution order into the constraint store. The constraint store con- tains a declarative specijication based on a temporal con- straint logic program. The store, thus, acts as a coordina- tion entity which on the one hand encapsulates the system synchronization requirements, and on the other hand, pro- vides a declarative specijication of the system concurrency issues, This provide great advantages in writing concurrent programs and manipulating them while preserving correct- ness. 1. Introduction Parallel computers and distributed systems are becom- ing increasingly important. Their impressive computation to cost ratios offer a considerable higher performance than that possible with sequential machines. Yet there are few commercial applications written for them. The reason is that programming in these environments is substantially more difficult than programming for sequential machines. When writing concurrent programs, the programmer, in addition to defining processes, has to specify process communica- tion and synchronization. Unlike sequential (or transforma- tional) programs, which merely terminate with a final re- sult, concurrent (or reactive) programs produce results dur- ing their execution and may not even be expected to termi- nate. Thus, while in traditional sequential programming the prpblem is reduced to make sure that the program’s final re- sult (if any) is correct and that the program terminates, in concurrent programming it is not necessary to obtain a final result but to ensure that several properties hold during pro- gram execution. These properties are classified into safety 0-7695-0583-WOO $10.00 0 2000 IEEE properties, those that must always be true, and progress (or liveness) properties, those that must eventually be true. Par- tial correctness and termination are special cases of these two properties. In traditional concurrent programming lan- guages both safety and prograss properties are indirecly pre- served by the use of modes, shared variables, etc. In addi- tion, synchronization concerns cannot in general be neatly encapsulated into a single unit which results in their im- plementation being scattered throughout the source code. This harms the readability of programs and severely compli- cates the development and maintenance of concurrent sys- tems. Furthermore, the fact that program synchronization concerns are intertwined with the rest of the code, also com- plicates the formal treatment of the concurrency issues of the program which directly affects the possibility of formal verification, synthesis and transformation of concurrent pro- grams. We believe that the intrinsic difficulties in writing concurrent systems can be considerably reduced by 0 using a declarative formalism to explicitly specify the system concurrency requirements, and 0 treating the system concurrency issues as orthogonal to the system base functionality. In this paper we propose a methodology for writing con- current applications in which the safety properties of a con- current program are declaratively stated as temporal con- straints. The constraints, on the one hand, encapsulate the system synchronization requirements, and on the other hand, provides a declarative specification of the system con- currency issues. Programs are annotated at points of inter- est so that the run-time environment can monitor and verify that specific synchronization constraints between the visit times of these points are enforced. The declarative nature of the constraints provides great advantages in reasoning about and deriving concurrent programs [6]. Constraints are language independent in that the concurrent applica- tion program can be specified in any imperative language such as the C programming language. Furthermore, the constraints are sufficiently high-level to allow the program- mer to glue together separate concurrent threads regard- 151

Transcript of [IEEE Comput. Soc Sixth IEEE International Conference on Engineering of Complex Computer Systems -...

Concurrent Programming Made Easy

Rafael Ramirez Andrew E. Santosa Roland H. C. Yap School of Computing

National University of Singapore (rafael,andrews, tyap} @comp.nus. edu.sg

Abstract The task of programming concurrent systems is substan-

tially more dificult than the task of programming sequen- tial systems with respect to both correctness and eflciency. In this paper we describe a constraint-based methodology for writing concurrent applications. A system is modeled as: (a) a set of processes containing a sequence of “mark- ers” denoting the processes points of interest; and (b) a constraint store. Process synchronization is specijied by in- crementally adding constraints on the markers’ execution order into the constraint store. The constraint store con- tains a declarative specijication based on a temporal con- straint logic program. The store, thus, acts as a coordina- tion entity which on the one hand encapsulates the system synchronization requirements, and on the other hand, pro- vides a declarative specijication of the system concurrency issues, This provide great advantages in writing concurrent programs and manipulating them while preserving correct- ness.

1. Introduction Parallel computers and distributed systems are becom-

ing increasingly important. Their impressive computation to cost ratios offer a considerable higher performance than that possible with sequential machines. Yet there are few commercial applications written for them. The reason is that programming in these environments is substantially more difficult than programming for sequential machines. When writing concurrent programs, the programmer, in addition to defining processes, has to specify process communica- tion and synchronization. Unlike sequential (or transforma- tional) programs, which merely terminate with a final re- sult, concurrent (or reactive) programs produce results dur- ing their execution and may not even be expected to termi- nate. Thus, while in traditional sequential programming the prpblem is reduced to make sure that the program’s final re- sult (if any) is correct and that the program terminates, in concurrent programming it is not necessary to obtain a final result but to ensure that several properties hold during pro- gram execution. These properties are classified into safety

0-7695-0583-WOO $10.00 0 2000 IEEE

properties, those that must always be true, and progress (or liveness) properties, those that must eventually be true. Par- tial correctness and termination are special cases of these two properties. In traditional concurrent programming lan- guages both safety and prograss properties are indirecly pre- served by the use of modes, shared variables, etc. In addi- tion, synchronization concerns cannot in general be neatly encapsulated into a single unit which results in their im- plementation being scattered throughout the source code. This harms the readability of programs and severely compli- cates the development and maintenance of concurrent sys- tems. Furthermore, the fact that program synchronization concerns are intertwined with the rest of the code, also com- plicates the formal treatment of the concurrency issues of the program which directly affects the possibility of formal verification, synthesis and transformation of concurrent pro- grams. We believe that the intrinsic difficulties in writing concurrent systems can be considerably reduced by

0 using a declarative formalism to explicitly specify the system concurrency requirements, and

0 treating the system concurrency issues as orthogonal to the system base functionality.

In this paper we propose a methodology for writing con- current applications in which the safety properties of a con- current program are declaratively stated as temporal con- straints. The constraints, on the one hand, encapsulate the system synchronization requirements, and on the other hand, provides a declarative specification of the system con- currency issues. Programs are annotated at points of inter- est so that the run-time environment can monitor and verify that specific synchronization constraints between the visit times of these points are enforced. The declarative nature of the constraints provides great advantages in reasoning about and deriving concurrent programs [6]. Constraints are language independent in that the concurrent applica- tion program can be specified in any imperative language such as the C programming language. Furthermore, the constraints are sufficiently high-level to allow the program- mer to glue together separate concurrent threads regard-

151

Start

No Yes

Start

Collect Data

Process P2

Figure 1. A medical application

less of their implementation language and application code. The constraints have a procedural interpretation which is based on the incremental and lazy generation of constraints, i.e., constraints are considered only when needed to reason about the execution order of current events.

Briefly, constraints can be specified between program points delineated by markers. For a marker M , t ime(M) (read as “the visit time at M”) denotes the time at which the program point defined by M is visited during execu- tion. In the following, we will refer to t ime(M) simply by M whenever confusion is unlikely. Informally, markers are associated with programs points between instructions, pos- sibly in different threads. Thus, given a pair of markers, constraints can be stated to specify their relative order of execution in all executions of the program.

Consider a medical application which consists of a pro- gram P that analyzes and diagnoses some data. The pro- gram consists of two processes P1 and P2. In principle, P can be specified in any imperative programming language and we may represent P1 and P2 as a sequence of func- tional blocks as in Figure 1.

The points of interest in the program are denoted by markers M1, . . . , M5 between which synchronization con- straints may be enforced. A constraint of interest in this system is that whenever the data must be collected before it can be analyzed. This requirement can be captured as a Constraint on the order in which markers M 1 and M5 are visited. This is, it suffices to specify the constraint

M5 < M1

in (order to satisfy the above requirement. X < Y can be read as “X precedes Y” or as “the execution of X precedes the execution of Y” (we will define the < operator in more detail in Section 3).

2. Related work

2.1. Traditional approaches to concurrent programming

While semaphores are useful for implementing synchro- nization primitives, they are too low-level to build large, re- liable systems. To alleviate this problem, programming lan- guage constructs such as monitors were introduced. Mon- itors are used to manage shared resources. The shared resources are accessed through the monitor’s interfaces. In some respect this matches object-orientation model, and thus monitors are not accidentally used in Java [13]. Although monitors are a higher abstraction compared to semaphores, building large, reliable concurrent programs is still a complex task. This partly due to the fact that moni- tors are not of a declarative nature which complicates their formal treatment.

A well accepted formalism for the specification and ver- ification of concurrent systems is Petri nets [18]. There are connections between this work and Petri nets. In fact, it has been shown that it is possible to automatically “trans- late” a Petri net specification into an equivalent constraint specification as described in the paper [ 161. However, Petri nets, as most graphical representations, tend to be difficult to ‘use when programming in the large and like semaphors and monitors indirecly preserve the system concurrency is- sues (in the case of semaphors and monitors by the use of shared variables and in the case of Petri nets by tokens).

2.2. Concurrent object-oriented program- ming

Various attempts have been done by the object-oriented community to separate concurrency issues from function- ality. Recently, some researchers have proposed aspect- orienfed programming (AOP) [ IO] which encompasses the separation of various program “aspects” (which include

152

synchronization) into codes written in “aspect languages” specific for the aspects. The aspect programs are later combined with the base language program using an aspect weaver. In this area, the closest related work is the work by De Volder and D’Hondt [21]. Their proposal utilizes a full-fledged logic programming language as the aspect lan- guage. In order to specify concurrency issues in the aspect language, basic synchronization declarations are provided which increase program readability. Unfortunately, the dec- larations have no formal foundation. This reduces consid- erably the declarativeness of the approach since correctness of the program concurrency issues directly depend on the implementation of the the declarations.

Traditionally, concurrent object-oriented programming paradigms are based on message passing. In contrast, ours is based on centralized coordination of processes. There are some, however, synchronization constructs in existing concurrent object-oriented languages that are comparable to ours. Close to our work are path expressions (e.g., PRO- COL [4]) and constructs similar to synchronization counters (e.g., Guide [ 121 and DRAGOON [2]). These proposals, as ours, differ from the AOP approach in that the specification of the system concurrency issues are part of the final pro- gram. Unfortunately, synchronization counters have limited expressiveness since they are not able to order events explic- itly. Path expressions are more expressive in this respect but they cannot express some important synchronization prob- lems (e.g., producers-consumers) without providing com- plex extensions for consulting program variables.

An important issue is that in all of the other proposals mentioned above, method execution is the smallest unit of concurrency. This is impractical in actual concurrent pro- gramming where we often need finer-grained concurrency.

2.3. Temporal constraints

Most of the previous work on temporal constraints for- malisms has concentrated on the specification and verifica- tion of real-time systems, e.g. RTL [9] and Hoare logic with time [ 191, and on the synthesis of real-time programs (e.g. TCEL (71, CFX [20], TimeC [14]. TimeC is the closest re- lated work. It is also based on the notion of markers, i.e. program points, but they are used as a mean to specify real- time requirements in a sequential program. Here we use markers for denoting program points of interest in order to specify the safety properties of a collection of processes in a concurrent program. This is done by associating events to markers and constraining the execution order of the events by imposing temporal constraints on them.

Section 3 describes the constraint language on which the methodology we propose is based. In Section 4 we give some examples which illustrate how the language may be

used to specify the concurrency issues (safety properties) of an application. Section 5 describes the methodology we propose. Section 6 outlines our current implementation, and finally Section 7 summarizes the contributions and indicates some areas of future research.

3. Logic programs for concurrent pro- gramming

In this section we overview the constraint language in which the system synchronization requirements are spec- ified. The main emphasis in the language is for it to be declarative on the one hand, and amenable to execution on the other. Unlike other approaches to concurrent program- ming, our proposal is concerned only with the specification of the concurrency issues of a system. This is, the applica- tion functionality is abstracted away and is assumed to be written in any conventional imperative programming lan- guage.

3.1. Events and constraints

Many researchers, e.g. [ 1 1, 151, have proposed methods for reasoning about temporal phenomena using partially or- dered sets of events. Our approach to concurrent program- ming is based on the same general idea. The basic idea here is to use a Constraint logic program (CLP) [SI in reason- ing of event order constraints. The constraints themselves (called precedence constraints) are of the form X < Y , read as “ X precedes Y” or “the execution of X precedes the execution of Y“, where X and Y are events, and < is a partial order.

The CLP is defined as follows. Constants range over events classes E , F’, . . . and there is a distinguished (post- fixed) functor +. Thus the terms of interest, apart from vari- ables, are e, e+, e + +, . . . , f , f+ , f + +, . . .. The idea is that e represents the first event in the class E , e+ the next event, etc. Thus, for any event X , X + is implicitly pre- ceded by X , i.e. X < X+. We denote by e ( + N ) the N-th event in the class E. Programs facts or predicate constraints are of the form p ( t 1 , . . . , tn) where p is a user defined pred- icate symbol and each ti is an event, i.e. a ground term. Program rules or predicute dejnitions are of the form

where the X i are distinct variables and B is a rule body re- stricted to contain variables in { X I , . . . , Xn} . A program is a finite collection of rules and is used to define a family of partial orders over events. Intuitively, this family is ob- tained by unfolding the rules with facts indefinitely ’, and

’ In general, reucriw concurrent programs on which we are focusing, do not terminate.

153

collecting the (ground) precedence constraints of the form e < f. Multiple rules for a given predicate symbol give rise to different partial orders. For example, since the following program has only one rule for p :

p ( e , f ). p ( E , F ) t E < F , p ( E + , F+).

it defines just one partial order e < f, e+ < f+, e + + < f + +, . . .. In contrast,

d e , f ). p ( E , F ) t E < F , p ( E + , F+). p ( E , F ) t F < E , p ( E + , F + ) .

defines a family of partial orders over {e, f , e+, f+, e + +, f + +, e+ + + . . .}. We will abbreviate the set of clauses

H t Csl, . . ., H t CS,

by the disjunction constraint (disjunction is specified by the usual disjunction operator ’;’):

The CLP has a procedural interpretation that allows a correct specification to be executed in the sense that agents run only as permitted by the constraints represented by the program. This procedural interpretation is based on an in- cremental execution of the program and a lazy generation of the corresponding partial orders. Constraints are generated by the CLP only when needed to reason about the execution times of current events. An interpreter for the CLP program is shown in Figure 2 (a full description of the procedural interpretation of CLP can be found in [ 171).

3.2. Markers and events

In order to refer to the visit times at points of interest in the program we introduce markers. A marker declaration consists of an event name enclosed by angle brackets, e.g. <e>. Markers annotations can be seen simply as program comments (i.e. they can be ignored) if only the functional semantics of an application is considered. Markers are as- sociated with programs points between instructions, possi- bly in different processes. Constraints may be specified be- tween program points delineated by these markers. For a marker M , t ime(M) (read as “the visit time at M”) denotes the current time at which the marker is visited. In the fol- lowing, we will refer to t ime(M) simply by M whenever confusion is unlikely. Given a pair of markers, constraints can be stated to specify their relative order of execution in all executions of the program. If the execution of a pro- cess PI reaches a program point whose execution time is

place precedence and predicate constraints

place events appearing as arguments of constraints

place predicate definitions in definition set DS; while TL not empty:

in constraint set CS;

in CS in try list TL;

delete first member e of TL; while e is neither enabled nor disabled in CS (e appears in user-defined constraint C):

replace C in CS by its definition in DS; add body variables to rear of TL; if e is enabled in CS then execute e else if e is conditionally enabled in CS

then reduce CS by e; execute e

else if e is unknown in CS then error.

for each precedence constraint in CS

(including those inside disjunctions):

Execute e:

with e on the left e < R:

delete e < R; add R to rear of TL if not in TL.

reduce CS by e: for each disjunction D in CS:

delete all alternatives of D in which e is disabled.

for a disjunction D: if e is enabled i n every alternative of D

else if e is disabled in every alternative of D

else if e is enabled in some alternatives

then e is conditionally enabled in D

then e is enabled in D

then e is disabled in D

of D and disabled in all others

else e is unknown in D.

for a conjunction CS: if e is disabled in at least one constraint in CS

then e is disabled in CS else if e is unknown in at least one constraint in CS

then e is unknown in CS else if e is conditionally enabled

in at least one constraint in CS then e is conditionally enabled in CS

else e is enabled in CS.

Figure 2. Constraint interpreter

154

constrained to be greater than the execution time of a not yet executed program point in a different process P2, pro- cess PI is forced to suspend execution. In the presence of loops and procedure calls a marker is typically visited sev- eral times during program execution. Thus, in general, a marker M associated with a program point p represents an event class E where each of its instances e, e+, e + + > . . corresponds to a visit to p during program execution (e rep- resents the first visit, e+ the second, etc.).

4. Synchronization constraints

In this section we illustrate how the language described is used to specify the concurrency issues (safety properties) of concurrent systems by presenting two examples.

Example 1. Consider implementation in Java of a stack data type. A basic specification without synchronization code is as follows (ignore for the moment markers <ai>, i.e. treat them as comments):

class Stack { static final int MAX = 10; int pos = 0; Object[] contents = new Object [ MAX I;

public void print 0 { <al> System.out.print(" [ ' I ) ;

for (int i=O ; i<pos; i++) {

System. out .print ( " I " ) ; <a2> 1 public Object peek 0 { <a3>

return contents [pos]; <a4> 1 public Object pop 0 { <a5>

return contents [--pos]; <a6> 1 public void push (Object e) { <a7>

contents [pos++]=e ; <a8> 1 1

System.out .print (contents [iI+" " ) ; 1

A specification of the class Stuck with synchronization code added is too complicated to fit comfortably onto a sin- gle page. For instance, a specification of the peek method with synchronization is as follows:

public Object peek 0 { while (true) {

synchronized (this) { if ((BUSYSOP == 0 ) & &

(BUSYqush == 0 ) ) { ++BUSY_peek; break; 11

try { wait ( ) ; 1 catch (InterruptedException e)

{ I 1 try { return contents [posl; 1

finally { synchronized (this

--BUSYgeek; notifyA11 0; 1

This specifies the safety property that the peek method waits until there are no more threads currently executing a push or a p o p method, i.e. mutual exclusion between peek and push and between peek and pop . It is clear that the syn- chronization code completely dominates the source code: almost all of the code for the peek method is synchroniza- tion code. Furthermore, it is very difficult to formally reason about the correctness of the code.

Similarly, the safety properties that no thread attempts to remove (pop) an item from an empty stack and no thread attempts to append (push) into a full stack would require coding of additional synchronization code in the pop and push methods.

We retain the sequential program and now consider the markers. The aforementioned safety property can be for- mally expressed by using temporal constraints as follows. The requirement that the peek method waits until there are no more threads currently executing apush or " p o p method may be implemented by

mutez(a3, a4, a5, a s ) . mutez(a3, a4, a7, as) . mutez(X1, X 2 , Y1, Y2) +

X 2 < Y1, m u t e z ( X l + , X2+ , Y1, Y2); Y2 < X I , mutez(X1, X 2 , Y1+, Y2+).

where a3, a 4 . . . a8 are the markers on our initial Java pro- gram. The requirement that no thread attempts to remove an item from an empty stack and no thread attempts to append into a full stack may be respectively implemented by

pop-empty(a8, a5) . pop-empty(A, B ) t A < B, p(A+, B+).

and

push-maz(a6, a7(+MAX)). push-maz(A, B ) t A < B, p ( A + , B+).

Note that is a predefined constant which value is fixed at compile time, as in the definition of Stuck (the constraints are not allowed to refer to program variables).

We explain how the constraint pop-empty works. The mechanism is similar for the p u s h m a r constraint. The con- straint pop-empty(a8, a5) specifies the (infinite) set of con- straints ( a 8 < a5, a8+ < a5+, a8 + + < a5 + +, . , .}. Thus, if during program execution marker a5 is visited for the first time by a thread 2'1 before marker a8 is visited, 2'1

will suspend at a5 due to constraint a8 < a5 until another thread T2 visits marker a8. Note that this is what we would expect: if no item has been pushed to the stack, i.e. no thread has visited marker a 8 , then no thread should be al- lowed to pop from the empty stack, i.e. treads must suspend

155

at a5 . We situation is similar for later visits to markers a5 and a8 .

Example 2. An example discussed in almost every text- book on concurrent programming (e.g. [ I ] ,[3]) is the pro- ducer and consumer problem. The problem considers two types of processes: producers and consumers. Producers create data items (one at a time) which then must be ap- pended to a buffer. Consumers remove items from the buffer (if it is not empty) and consume them, i.e. perform some computation which uses the data item. Here, we consider a system with one producer and one consumer (generaliza- tion to the general case is straightforward). Thus, the pro- ducer process can be defined by an infinite cycle containing pToduce (producing an item) and append (appending the item to the buffer). Similarly, the consumer process can be defined by an infinite cycle containing r e m o v e (remov- ing an item from the buffer) and consume (consuming the item). The producer and consumer may be defined as fol- lows (they have been annotated with markers p l , p 2 , c l and c2) .

Producer Consumer z repeat repeat

produce (X) ; < c l >

append-item(X); < c2 > < p l > remove-item( X ) ;

< p2 > consume(X) forever forever

If we assume an infinite buffer, the only safety property needed is that the consumer never attempts to remove an item from an empty buffer. This property can be expressed by

bufler(p2, c l ) . bufler(P, C ) t P < C , buf l e r (P+ , C+).

In practice however, buffers are finite. A bounded buffer can store only a finite number of data elements. Thus, in practice, an extra safety property is that the producer at- tempts to append items to the buffer only when the buffer is not full. For instance, this safety property for a system with a buffer of size 3 can be expressed by

b u f e r ( c 2 , p l f ++). bufler(C, P ) t C < P , buf l e r (C+ , P+).

5. The methodology

The treatment of concurrency issues as orthogonal to the rest of the code allows programmers to independently de- velop one component from the another. A programmer may

start implementing a system either by defining a set of ab- str,act processes and their synchronization constraints, or by writing the code for a set of processes, and later identifying the: points of interest in their code and introducing synchro- nization constraint constraints among these points. This is

ID The concurrency-first approach:

1. define a skeleton of the system processes.

2. based on the skeleton, specify the required in- terprocess synchonization using temporal con- straints.

3. implement the processes functionality.

I. The process-first approach:

I . implement a set of sequential processes.

2. based on this implementation, identify the pro- gram points of interest and introduce constraints on the execution order of these points.

An important issue in modern software engineering is reusability. The fact that concurrency issues are separated from the code minimizes dependency between application functionality and concurrency control and introduces the possibility of reusing both software components. It is, in general, possible to test different synchronisation schemes without modifying the code of the operations, and con- versely, a synchronisation scheme may be reused by several applications.

5.1. The concurrency-first approach

Using our programming system, programmers first de- fine an skeleton of the system processes (or threads) and de- fine the concurrency constraints among them in a file, e.g., rules. t x t . The file contains precedence constraints, pre:dicate constraints and predicate definitions. For instance, the processes and synchronization constraint specification in the readers-writers problem with two writers and one reader is shown below.

Reader1 = Writer1 Writer2 repeat repeat repeat

compl ( X ) ; c o m p 2 ( X ) ; comp3(X) ; < a > < C > < e > r e a d ( X ) ; w r i t e ( X ) ; wr i te (X); < b > < d > < f >

forever forever forever

# constraints mutex(a, b, c , d ) mutex(a,b,ef) mutex(c,d, e, f)

156

Figure 3. Constraints input panel

Figure 4. Concurrency constraints simulator panel ## predicate definitions murex( K X , XZ) t W < Y, murex( W+,X+, XZ);

Z < W, murex(KX, Y+ ,Z+) .

The reader thread contains two markers a and b at the be- ginning and end of its critical section. The writers have two markers each c and d, and e and f respectively, which mark the beginning and end of their critical section. In rules. txt we specify the constraints that must be sat- isfy by any program execution. For example, the constraint mutex(a,b,c,d) state mutual exclusion between the read and write of the reader and the first writer.

The task of designing concurrency constraints can be eased using our web-based tool which user inter- face is shown in Figures 3 and 4. It is located at the URL http://www.comp.nus.edu.sg/-rafa- el/ tempo/main. html. The users may input the pro- cesses scheletons, constraints and predicate definitions us- ing constraints input panel in Figure 3 and can visualize the execution of the processess in the simulator panel of Figure 4. Once the user is happy with the specification the con- straints can be saved into the file rules. txt.

The programmer can implement the system functionality either before, after or in parallel with the specification of synchronization constraints.

5.2. The process-first approach

In this approach, concurrent applications can be devel- opped by firstly specifying a set of sequential processes and later identifying the program points of interest and introduc- ing constraints on the execution order of these points. for instance, Example 1 shows an implementation of a stack class (we assume the threads executing the class methods are already defined) and only when the implementation is completed the programer indentifies the program points of interest and label them with markers. The resulting code is compiled to the code shown in Figure 5. The required syn- chonization constraints (as in Example 1) are stored in the file rule. txt.

6. Implementation A prototype implementation of the ideas presented here

has been written using Java. Java was used both to imple- ment the constraint language and to write the code of a num- ber of applications.

157

import tempo. * ;

mblic class Stack { static final int MAX=10; int pos=O; Object[] contents=new Object[MAXl; Tempo tempo;

public Stack0 {

1 public Object peek0 {

this. tempo = new Tempo ( "C : \rule. txt " ) ;

try ( tempo. check ( "A" ) ; return contentslposl;

tempo. check ( "B" ) ; 1 finally {

1 1 public Object pop 0 (

try ( tempo. check ( " C " ) ; return contents [--pos];

tempo . check ( " D " ) ; } finally (

1 1 public void push (Object e) (

try { tempo. check ( " E " ) ; contents [pos++l =e;

tempo. check ( " F " ) ; } finally (

1 1

}

Predicate Definition Store (DS)

...........................................................

Dynamic Stores (CS)

Figure 5. Resulting Java code

6.1. Architecture

The runtime architecture of our implementation is shown in Figure 6. It consists mainly of three parts:

0 The event interface is an object which decides whether or not threads suspend upon reaching a marker during execution. When a thread reaches a marker m, a re- quest is sent to the interface to determine whether the current event e associated with m is disabled, i.e. it appears on the right of a precedence constraint X < e , or enabled, i.e. otherwise, wrt the constraint store. If e is found to be disabled, the thread is blocked until e becomes enabled, otherwise the thread proceeds exe- cution at the instruction immediately after the m.

0 The constraint store contains the system synchroniza- tion constraints.

0 The user program is the main program and typically

I Event Interface I User Threads

checkra") check("c") j

Thread n

I I I

Thread n

I I I

I I 1 I I I

............. I I I

7 7 7 : .....................................................................................

Figure 6. Runtime architecture

specifies the system synchronization constraints, cre- ates the event interface and spawns a number of threads which may contain markers.

Additionally, in program development our system has a

verifer that produces the aforementioned runtime compo- nents (see Figure 7). The verifier examines the specifica- tion of the system synchronization constraints to detect any errors such as infinite loops in predicate definitions, e.g. p ( . X ) t p ( X ) , before the event interface is created.

The overall runtime mechanism is as follows: Once the event interface has been created and a set of threads have been spawned, whenever one of the threads reaches a marker in their code, a communication with the constraint store is triggered. Currently, the communication is imple- mented as a request to the event interface. The request is of the form check(Str), in which Str is a string denot- ing marker's identifier (e.g., "pl," ''p2," "cl," "c2" in the producers-consumers example above). This request have two differing effects:

1 . The request fails causing the invoking thread to sus- pend if the current event associated with Str is dis- abled according to the constraint store.

2. The request succeeds if the current event associated with Str is enabled according to the constraint store.

158

Multi-Threaded User Program

Verifier Q Multi-Threaded j Event Interface & User Program : Constraint Store Source Code : Code Im oits

Figure 7. Verifier

In this case, the treads proceeds execution at the in- struction immediately after the marker and relevant suspended threads are awaken allowing them to re- check the constraint store.

The decision on whether to suspend a thread or not is based on a procedural interpretation of the constraint logic programs used to specify synchronization constraints. The procedural interpretation allows a correct specification to be executed in the sense that events are only executed as per- mitted by the constraints represented by the program, as ex- plained before. This procedural interpretation is based on an incremental execution of the program and a lazy gen- eration of the corresponding partial orders. Constraints are generated by the constraint logic program only when needed to reason about the execution times of current events. Fig- ure 8 shows the actual interpreter for a constraint logic pro- gram specifying the synchronization constraints of a Java program. We omit parts that are identical to the interpreter already introduced in Section 3.1. The main difference be- tween the interpreter presented in Section 3.1 and the one presented in this section is that hare, events are lazily tried only when a thread visits a marker associated with the event (in Section 3.1, events are tried eagerly as soon as they reach the head of the try list).

To see how the interpreter works, let us use the producers-consumers example explained in Section 4. Ini- tially, DS contains the predicate definition p ( X , Y ) t X < Y , p ( X + , Y + ) , and CS contains two predicate con- straints: p(p2,cl) and p(c2,pl + ++). Suppose a con- sumer thread reaches marker cl . At this point, the constraint store is checked to determine whether c l is enabled or dis- abled which requires the expansion of p(p2,cl). Based on the predicate definition in DS, p(p2,cl) is expanded to p 2 < cl,p(p2+,cl+) at which point c l is found to be disabled since it appears on the right hand side of the

place system constraints in constraint store CS; place predicate definitions in definition store DS; when thread T reaches marker E:

get current occurrence e of the event class

while e is neither enabled nor disabled in CS (e appears in user-defined constraint C):

replace C in CS by its definition; if e is disabled in CS then suspend T if e is enabled in CS then execute e else if e is conditionally enabled in CS

associated with E;

then reduce CS by e; execute e

else if e is unknown in CS then error.

Execute e for each precedence constraint in CS with e

on the left e<R: (including those inside disjunctions):

delete e<R; resume treads waiting for R.

Figure 8. Implemented constraints interpreter

precedence constraint p2 < cl . Thus, execution of the con- sumer thread is suspended at marker cl . At some point, a producer thread reaches marker p l and after checking the constraint store it is determined that p l is enabled (it does not appear on the right hand side of any precedence con- straint). It then proceeds to add an item to the buffer, and then it reaches marker p2. Since p2 is enabled execution of the producer thread proceeds, constraint p 2 < c l is deleted from CS since it has already been satisfied, and threads sus- pended for event cl are awaken so they can re-checks the constraint store. At this point, since c l does not appear on the right side of any precedence constraints, the consumer thread may continue its execution. This has the effect of not allowing retrieval of items when the buffer is empty. A sim- ilar situation occurs when a producer thread attempts to add an item to a full buffer.

6.2. Performance

Our implementation is still in a prototype stage, thus sev- eral efficiency issues have still to be addressed. However, the performance of our current implementation is accept- able. The perfomance issues have been described in an ac- companying paper.

159

6.3. Fairness

Fairness is implicitly guaranteed by our implementation. Every event that becomes enabled will eventually be exe- cuted (provided that the program point associated with it is reached). This is implemented by dealing with event execu- tion requests in a first-in-first-out basis. Although fairness is provided as the default, users, however, may intervene by specifying priority events using temporal constraints (on how to do this, see [ 171). It is therefore possible to specify unfair scheduling.

7. Conclusion We have described a constraint-based methodology for

writing concurrent applications. The methodology is based on a first-order logic constraint language for expressing syn- chronization constraints in concurrent programs. The safety properties of the system are declaratively stated as prece- dence constraints. Programs are annotated at points of in- terest so that the run-time environment enforces specific temporal relationships between the order of these points. Constraints are language independent in that the application program can be specified in any conventional concurrent language. The constraints have a procedural interpretation that allows the specification to be executed. The procedural interpretation is based on the incremental and lazy genera- tion of constraints, i.e. constraints are considered only when needed to reason about the execution time of current events. In this framework, concurrent applications can be devel- opped by firstly specifying a set of sequential processes and later identifying the program points of interest and introduc- ing constraints on the execution order of these points. Al- ternatively, firstly a skeleton of the system processes can be defined and the required interproces synchonization speci- fied by temporal constraints and once this is finalized, the processes functionality can be implemented.

References

[ I ] Andrews, G. R. 1991. Concurrent Programming: Principles and Practice. BenjamidCummings.

[2] Atkinson, C. 199 1. Object-Oriented Reuse, Concurrency and Distribution: An Ada-Based Approach. Addison- Wesley.

[3] Ben-Ari, M. 1990. Principles of Concurrent and Distributed Programming . Prentice Hal I.

[4] Van den Bos, J. and Laffra, C. 1989. PROCOL: A paral- lel object language with protocols. ACM SIGPLAN Notices 24(10):95-112, October 1989. Proc. of OOPSLA '89.

[5] Gregory, S. and Ramirez, R. 1995. Tempo: a declara- tive concurrent programming language. Proc. of the ICLP (Tokyo, June), MIT Press, 1995.

[6] Gregory, S. 1995. Derivation of concurrent algorithms in Tempo. In LOPSTR95: Fifth International Workshop on Logic Program Synthesis and Transformation.

[7] Hong, S. and Gerber, R. 1995. Compiling real-time pro- grams with timing constraint refinement and structural code motion, IEEE Transactions on Software Engineering, 21.

[8] Jaffar, J. and Maher, M. 1994. Constraint logic program- ming: A survey. Journal of Logic Programming, Special lofh Anniversary Issue, Vols 19/20.

131 Jahnaian F. and Mok A. K. 1987. A graph theoretic approach for timing analysis and its implementation, IEEE Transac- tions on Computers, C36(8).

[IO] Kiczales, G., Lamping, J., Mendhekar, A., Maeda, C., Lopes, C., Loingtier, J.-M. and Irwin, J. 1997. Aspect-oriented pro- gramming. In ECOOP '97-Object-Oriented Programming, Lecture Notes in Computer Science, number 1241, pp. 220- 242, Springer-Verlag.

[I I ] Kowalski, R.A. and Sergot, M.J. 1986. A logic-based calcu- lus of events. New Generation Computing 4, pp. 67-95.

I21 Krakowiak, S., Meysembourg, M., Nguyen Van, H., Riveill, M., Roisin, C. and Rousset de Pina, X. 1990. Design and im- plementation of an object-oriented strongly typed language for distributed applications. Journal of Object-Oriented Pro- gramming 3(3):11-22.

131 Lea, D. 1996. Concurrent Programming in Java: Design Principles and Patterns. Addison- Wesley.

141 Leung, A., Palem, K. and Pnueli, A. 1998. Time C: A Time Constraint Language for ILP Processor Compilation, Tech- nical Report TR1998-764, New York University.

IS] Pratt, V. 1986. Modeling concurrency with partial orders. International Journal of Parallel Programming 15, 1 , pp. 33- 71.

Nets, logic and concurrent object- oriented programming. Proceedings of the First Workshop on Object-Oriented Programming and Models of Concur- rency, Turin.

[ 1'71 Ramirez, R. 1996. A logic-based concurrent object-oriented programming language, PhD thesis, Bristol University.

[I81 Reisig, W. 1985. Petri nets, an introduction. Springer- Verlag.

[19] Shaw, A. 1989. Reasoning about time in higher-level lan- guage sofhvare, IEEE Transactions on Software Engineer- ing, lS(7).

[20] Stoyenko, A. D., Marlowe, T. J. and Younis, M. F. 1996. A language for complex real-time systems, Technical Report cis9521, New Jersey Institute of Technology.

[21] De Volder, K. and D'Hondt, T. 1999. Aspect-oriented logic meta programming. In Meta-Level Architectures and Reflec- tion, Lecture Notes in Computer Science number 1616, pp. 250-272. Springer-Verlag.

[I61 Ramirez, R. 1995.

160