A property is an attribute of a program that is true for every possible execution of that program. Properties of interest for concurrent programs fall into two categories: safety and liveness. A safety property asserts that nothing bad happens during execution. A liveness property asserts that something good eventually happens. Another way of putting this is that safety is concerned with a program not reaching a bad state and that liveness is concerned with a program eventually reaching a good state.
In sequential programs, the most important safety property is that the final state is correct. We have already seen that for concurrent programs, important safety properties are mutual exclusion and the absence of deadlock. In the previous chapter, we determined that deadlock is generally a bad state from which no further actions can be executed. In Chapter 4, we saw that allowing more than one process to access a shared variable resulted in interference and thus incorrect or bad states.
The most important liveness property for a sequential program is that it eventually terminates. However, in concurrent programming, we are frequently concerned with systems that do not terminate. In this chapter, we primarily deal with liveness issues relating to resource access: are process requests for shared resources eventually granted? We will see that liveness properties are affected by the scheduling policy that determines which of a set of eligible actions are chosen for execution.
This chapter explains how we can analyze the finite state models of concurrent systems for both safety and liveness problems. The example of cars crossing a single-lane bridge is used to focus discussion. We then analyze a number of implementations of read/write locks. Read/write locks allow many processes to access a shared resource at the same time for read access, but require write access to be mutually exclusive. They occur in many concurrent programs.
In the previous chapter, we saw that the LTSA analysis tool performs deadlock analysis using a breadth-first search on the labeled transition system corresponding to an FSP model. The "bad" states that are the objective of this search are those states with no outgoing transitions, i.e. deadlock states. In addition to deadlock states, the search is looking for ERROR
states. These are distinguished in the LTS by having a unique identity (−1). So far in the book, we have used the ERROR
state explicitly denoted by the local process ERROR
. In Chapter 4, we specified a test process that explicitly caused a transition to ERROR
when it detected erroneous behavior. In Chapter 5, the ERROR
state was used to detect when a finite range was exceeded. The example given in Figure 7.1 is an actuator that must not receive a second command before it has responded to the first.
With no control system to ensure that the actuator does not receive multiple unacknowledged commands, safety analysis performed by LTSA produces the following trace:
Trace to property violation in ACTUATOR: command command
In the test process of Chapter 4, the range excess of Chapter 5 and the actuator example of Figure 7.1, we have specified the situations regarded as errors rather than directly expressing the required safety property that we wish to preserve. In the actuator example, the safety property is that a command
action must always be followed by a respond
action without an intervening command
. In complex systems it is usually better to specify safety properties by stating directly what is required rather than stating what is not required. In this way, we can concentrate on the desired behavior of a system rather than trying to enumerate all the possible undesirable behaviors.
Safety properties are specified in FSP by property processes. Syntactically, these are simply FSP processes prefixed by the keyword property. They are composed with a target system to ensure that the specified property holds for that system. The example of Figure 7.2 specifies the property that it is polite to knock before entering a room.
The LTS diagram of Figure 7.2 reveals that in translating a property process, the compiler automatically generates the transitions to the ERROR
state. It is easily seen that an enter
action before a knock
action causes a transition to the ERROR
state. In addition, knocking twice is a violation of the POLITE
property. It should be noted that in every state of the property process of Figure 7.2, all the actions in its alphabet (enter, knock
) are eligible choices. Those that are not part of the behavior allowed by the safety property are transitions to the ERROR
state. This is true of all property processes.
The property to check the correct operation of the ACTUATOR
of Figure 7.1 is simply:
property
SAFE_ACTUATOR
=(command->respond->SAFE_ACTUATOR).
Property processes may be composed with a system without affecting the correct behavior of that system. In other words, composing a property process with a set of processes does not affect their normal operation. However, if behavior can occur which violates the safety property, then a transition to the ERROR
state results. To preserve this transparency of safety properties, property processes must be deterministic. That is they must not contain non-deterministic choices. Experience has shown that this is rarely a restriction in practice.
Asafety property
P defines a deterministic process that asserts that any trace including actions in the alphabet of P
, is accepted by P
.
Thus, if P
is composed with S
, then traces of actions that are in the alphabet of S
and the alphabet of P
, must also be valid traces of P
, otherwise ERROR
is reachable. We can specify that an action or a set of actions should never happen by using alphabet extension. The example of Figure 7.3 asserts that disaster
should never happen.
Listed below is a slightly modified version of the SEMADEMO
model used in section 5.2.1 to show how semaphores are used to ensure mutual exclusion. The modification is to replace the single action critical
with the actions enter
and exit
which model the entry and exit to the critical section in which mutually exclusive access to a shared resource is required.
LOOP = (mutex.down->enter->exit->mutex.up->LOOP). ||SEMADEMO = (p[1..3]:LOOP ||{p[1..3]}::mutex:SEMAPHORE(1)).
To verify that this system does in fact ensure mutual exclusion, we can specify a mutual exclusion property and compose it with the system as follows:
property
MUTEX =
(p[i:1..3].enter->p[i].exit->MUTEX).
||CHECK = (SEMADEMO || MUTEX).
The safety property MUTEX
specifies that when a process enters the critical section (p[i].enter
), the same process must exit the critical section (p[i].exit
)before another process can enter. The property is not violated in the system as it stands; however, if we change the value with which the semaphore is initialized from one to two (i.e. SEMAPHORE(2)
) then safety analysis using LTSA produces the following trace:
Trace to property violation in MUTEX: p.1.mutex.down p.1.enter p.2.mutex.down p.2.enter
The trace is clearly a violation of mutual exclusion since two processes have entered the critical section.
The problem is depicted in Figure 7.4. A bridge over a river is only wide enough to permit a single lane of traffic. Consequently, cars can only move concurrently if they are moving in the same direction. A safety violation occurs if two cars moving in different directions enter the bridge at the same time.
To clarify the discussion, we refer to cars moving from left to right as red cars and cars moving from right to left as blue cars (see demonstration applet). In our concurrent-programming model, each car is a process and the problem is to ensure that cars moving in different directions cannot concurrently access the shared resource that is the bridge. To make the simulation more realistic, we must also ensure that cars moving in the same direction cannot pass each other.
In the following section, we develop a model of the system and a Java implementation that corresponds to the model. In this section, we are concerned primarily with the safety properties of the problem. Later in the chapter, we will address liveness issues.
In modeling the single-lane bridge, we use the following constant and range definitions:
const
N = 3// number of each type of car
range
T = 0..N// type of car count
range
ID= 1..N// car identities
The essence of the problem is access to the bridge, so the only events of interest in which a car participates are entering the bridge and leaving the bridge. Consequently, a car is modeled by a process that repeatedly enters and leaves the bridge:
CAR = (enter->exit->CAR).
To model the fact that cars cannot pass each other on the bridge, we require the following processes which constrain the order of the enter
and exit
actions respectively:
NOPASS1 = C[1],//preserves entry order
C[i:ID] = ([i].enter->C[i%N+1]). NOPASS2 = C[1],//preserves exit order
C[i:ID] = ([i].exit->C[i%N+1]). ||CONVOY = ([ID]:CAR||NOPASS1||NOPASS2).
The CONVOY
process models a set of cars traveling in the same direction that enter the bridge one after the other and leave the bridge one after the other. However, it does not stop one car exiting the bridge before the next car enters. The behavior of all cars is captured by the following composition:
||CARS = (red:CONVOY || blue:CONVOY).
The remaining entity that must be modeled is the bridge itself. This must constrain CARS
so that although one or more cars moving in the same direction may be on the bridge concurrently, cars moving in different directions may not. To enforce this, the bridge maintains a count of blue cars on the bridge and of red cars on the bridge. Red cars are only allowed to enter when the blue count is zero and vice versa. The BRIDGE
process is listed below:
BRIDGE = BRIDGE[0][0], //initially empty
BRIDGE[nr:T][nb:T] = //nris the red count,
nbthe blue
(when
(nb==0) red[ID].enter -> BRIDGE[nr+1][nb] |red[ID].exit -> BRIDGE[nr-1][nb] |when
(nr==0) blue[ID].enter-> BRIDGE[nr][nb+1] |blue[ID].exit -> BRIDGE[nr][nb-1] ).
Note that the exit
actions of the bridge permit the car counts, nr
and nb
, to be decremented even though their value is 0. As described in Chapter 5, the FSP compiler in the LTSA tool will automatically map these undefined states to the ERROR
state, indicating this by issuing warnings:
Warning - BRIDGE.-1.0 defined to be ERROR Warning - BRIDGE.0.-1 defined to be ERROR ...
In fact, when BRIDGE
is composed with CARS
, their behavior prevents cars which have not entered from exiting and the ERROR
state is unreachable.
Before describing the overall system composition, we need to specify a safety property to compose with the system that verifies that cars do not collide on the bridge. The required property is listed below. It specifies that while red cars are on the bridge only red cars can enter and while blue cars are on the bridge only blue cars can enter. When the bridge is empty, either a red car or a blue car may enter. The index i
is used to count the red (or blue) cars currently on the bridge.
property
ONEWAY =(red[ID].enter -> RED[1] |blue[ID].enter -> BLUE[1] ), RED[i:ID] = (red[ID].enter -> RED[i+1] |when
(i==1)red[ID].exit -> ONEWAY |when
(i>1) red[ID].exit -> RED[i-1] ),
BLUE[i:ID]= (blue[ID].enter -> BLUE[i+1] |when
(i==1)blue[ID].exit -> ONEWAY |when
(i>1) blue[ID].exit -> BLUE[i-1] ).
The entire system can now be modeled by the composite process specified in Figure 7.5.
Safety analysis using LTSA verifies that the ONEWAY
safety property is not violated.
However, without the constraints provided by the BRIDGE
, the composition (CARS||ONEWAY)
yields the following safety violation:
Trace to property violation in ONEWAY: red.1.enter blue.1.enter
In the single-lane bridge problem, it is reasonably clear which are the active entities and which are the passive entities. Cars are implemented as Java threads and the bridge as a monitor. This leaves the model entities NOPASS1
and NOPASS2
concerned with constraining overtaking. These have no explicit representation in the implementation. The overtaking constraint is dealt with in the BridgeCanvas
class which displays car movement. Figure 7.6 depicts the class diagram for the program.
An instance of the BridgeCanvas
class is created by the SingleLaneBridge
applet. A reference to it is passed to each newly created RedCar
and Blue-Car
object. The methods provided by the BridgeCanvas
class are listed in Program 7.1.
Example 7.1. BridgeCanvas
class.
class
BridgeCanvasextends
Canvas {public
void init(int ncars) {...}//set number of cars
//move red car with the identity
ia step
//returns true for the period from just before,
// until just after car on bridge
public
boolean moveRed(int i)throws
InterruptedException{...}//move blue car with the identity
ia step
//returns true for the period from just before,
//until just after car on bridge
public
boolean moveBlue(int i)throws
InterruptedException{...}public synchronized
void freeze(){...}//freeze display
public synchronized
void thaw(){...}//unfreeze display
}
Example 7.2. RedCar
and BlueCar
classes.
class
RedCarimplements
Runnable { BridgeCanvas display; Bridge control; int id; RedCar(Bridge b, BridgeCanvas d, int id) { display = d; this.id = id; control = b; }public
void run() {try
{while
(true) {while
(!display.moveRed(id));// not on bridge
control.redEnter();// request access to bridge
while
(display.moveRed(id));// move over bridge
control.redExit();// release access to bridge
} }catch
(InterruptedException e) {} } }class
BlueCarimplements
Runnable { BridgeCanvas display; Bridge control; int id; BlueCar(Bridge b, BridgeCanvas d, int id) { display = d; this.id = id; control = b; }public
void run() {try
{while
(true) {while
(!display.moveBlue(id));// not on bridge
control.blueEnter();// request access to bridge
while
(display.moveBlue(id));// move over bridge
control.blueExit();// release access to bridge
} }catch
(InterruptedException e) {} } }
The code for the two classes representing cars is listed in Program 7.2. Each car moves until it is about to enter the bridge. It then requests access to the bridge by invoking control.redEnter()
if it is a red car and control.blueEnter()
if it is blue. When a car has crossed the bridge, it invokes the appropriate Exit
method.
Example 7.3. Bridge
class.
class
Bridge {synchronized
void redEnter()throws
InterruptedException {}synchronized
void redExit() {}synchronized
void blueEnter()throws
InterruptedException {}synchronized
void blueExit() {} }
The entry and exit bridge access methods are provided by an instance of the class Bridge
or a class derived from Bridge
. Class Bridge
provides a null implementation as listed in Program 7.3. This enables us to view the results of an unsafe bridge implementation.
The SingleLaneBridge
applet class creates one, two or three of each of the red and blue cars depending on which button is clicked. In addition, the check boxes select an implementation of the bridge monitor. Figure 7.7 depicts the consequences of using the null implementation of Program 7.3.
Clicking the Safe check box creates a system that avoids the collisions of Figure 7.7. It uses the class SafeBridge
which is a direct translation of the BRIDGE
process from the model. Each of the guarded actions from the model becomes a conditionally synchronized method. Since this has been modeled and shown to preserve the ONEWAY
safety property, we can be reasonably confident in the safety of the SafeBridge
implementation. The code for the class is listed in Program 7.4.
Example 7.4. SafeBridge
class.
class
SafeBridgeextends
Bridge {private
int nred = 0;//number of red cars on bridge
private
int nblue = 0;//number of blue cars on bridge
//Monitor Invariant:
nred≥0and
nblue≥0and
//
not
(nred>0and
nblue>0)synchronized
void redEnter()throws
InterruptedException {while
(nblue>0) wait(); ++nred; }synchronized
void redExit(){ --nred;if
(nred==0)notifyAll(); }synchronized
void blueEnter()throws
InterruptedException {while
(nred>0) wait(); ++nblue; }synchronized
void blueExit(){ --nblue;if
(nblue==0)notifyAll(); } }
The implementation of SafeBridge
uses conditional notification. We only wake up waiting threads when the number of cars on the bridge – either red or blue – is decremented to zero. This avoids unnecessary thread switches since otherwise, blue cars would be woken up every time a red car leaves the bridge and vice versa. It is only the last car of a particular color to leave the bridge that should wake up waiting car threads.
SafeBridge
ensures that cars do not collide on the bridge; however, it does not ensure that cars eventually get the opportunity to cross the bridge. With three cars of each color, if a red car crosses the bridge first there is always a red car on the bridge and consequently, blue cars never get to cross. This situation is called starvation: a form of liveness property discussed in the next section.
A liveness property asserts that something good eventually happens. A reasonable liveness property for the single-lane bridge would be that all red and blue cars eventually get to cross the bridge. As we have seen, the program developed in the previous section does not satisfy this property in the situation where there are three cars of each type. In this section, we see how models can be analyzed for liveness. Like deadlock and other safety properties, the objective is to solve liveness problems at the modeling stage so that they do not occur in the implemented program.
A completely general treatment of liveness is rather involved and requires the use of a temporal logic to specify the required liveness properties. Rather than burden the reader with another formalism, we deal with a restricted class of liveness properties which we term progress. A progress property asserts that whatever state a system is in, it is always the case that a specified action will eventually be executed. Progress is the opposite of starvation, the name given to a concurrent-programming situation in which an action is never executed. Progress properties are simple to specify and are sufficiently powerful to capture a wide range of liveness problems in concurrent programs.
To illustrate the notion of progress, we use a simple example, that of tossing a coin. The model is depicted in Figure 7.8.
If the coin were tossed an infinite number of times, we would expect that heads would be chosen infinitely often and that tails would be chosen infinitely often. In fact, this depends on the scheduling policy used to decide on which transition from the set of eligible transitions should be executed. If the policy is not fair then we could always choose the toss
transition leading to heads
. We assume that the scheduling policy for choice is fair as defined in the following:
Fair Choice: If a choice over a set of transitions is executed infinitely often, then every transition in the set will be executed infinitely often.
If the transition (or transitions) of an action occurs infinitely often in a system, we can say that it is always the case at any stage of the execution that the action will eventually occur. With the assumption of fair choice then the coin-tossing system should eventually choose heads and eventually choose tails. We can assert this with progress properties specified in FSP. A progress property is defined by:
progress
P={a1,a2..an} defines a progress property P
which asserts that in an infinite execution of a target system, at least one of the actions a1,a2..an
will be executed infinitely often.
In other words, a progress property asserts that at any stage of execution one of the actions in the progress set will eventually occur. The liveness requirement for coin tossing can now be expressed as:
progress
HEADS = {heads}progress
TAILS = {tails}
The COIN
system we have defined so far satisfies these properties. We now examine a system that does not. Suppose that the agent which tosses the coin first picks one of two coins: a normal coin with a head and a tail as defined in Figure 7.8 and a trick coin which has a head on both sides. The outcome of tossing the trick coin must always be heads. This system is modeled in Figure 7.9.
Progress analysis of the TWOCOIN
system against the progress properties HEADS
and TAILS
produces the following output:
Progress violation: TAILS Path to terminal set of states:
pick Actions in terminal set: {toss, heads}
This confirms the expected result: if the agent picks the trick coin then the action tails
will never occur. This is of course a violation of the TAILS
progress property, which asserts that in an infinite execution, tails must occur infinitely often. The reader should note that the system of Figure 7.9 does not violate the progress property:
progress
HEADSorTAILS = {heads,tails}
Property HEADSorTAILS
is not violated since only one of the actions in the progress set need be executed infinitely often to satisfy the property.
Progress analysis involves first performing a search for terminal sets of states.
A terminal set of states is one in which every state is reachable from every other state in the set via one or more transitions and there is no transition from within the set to any state outside the set.
In graph theory, this is known as a strongly connected component, which has no path to any nodes outside the set of nodes in the component. For example, the labeled transition system of Figure 7.9 has two terminal sets of states, {1, 2}, which are the states relating to the trick coin, and {3, 4, 5}, which are the states relating to the normal coin.
An execution of a system represented by a finite set of states can only be infinite if some of the states are visited infinitely often. The states that are visited infinitely often in an execution must form a terminal set. Given fair choice, each terminal set of states represents an execution in which each transition in the set is executed infinitely often. Since there is no transition out of a terminal set, any action that is not used in all terminal sets cannot occur infinitely often in all executions of the system. Checking that a progress property holds is now simply checking that in each terminal set, at least one of the actions in the progress set occurs as a transition. Conversely, a progress property is violated if analysis finds a terminal set of states in which none of the progress set actions appear. For the TAILS
property, this terminal set is the set of states {1, 2} in which the action tails
does not occur. The output gives the shortest execution path to the root of the terminal set and lists the actions that do appear in the set.
If no progress properties are specified, LTSA performs progress analysis using a default property. This property asserts that for every action in the alphabet of the target system, given fair choice, that action will be executed infinitely often. This is equivalent to specifying a separate progress property for every action. For the TWOCOIN
system, this default analysis produces the following output:
Progress violation for actions: {pick} Path to terminal set of states: pick Actions in terminal set: {toss, heads, tails} Progress violation for actions: {pick, tails} Path to terminal set of states: pick Actions in terminal set: {toss, heads}
The analysis produces two progress violations since the action pick
is not executed infinitely often in either terminal set. The value of this default property is that if it is not violated, then no specified progress properties can be violated. In other words, if the default property holds, then every other progress property, specified in terms of subsets of the action alphabet of a target system, must also hold. This is true since the default property asserts that every action is executed infinitely often. All systems in which the states occur inside a single terminal set satisfy the default progress property.
If default progress analysis is applied to the single-lane bridge model then no violations are detected. However, we know from the implementation that it is possible for progress violations to occur. Either the blue cars or the red cars may wait forever to cross the bridge. Why do we not detect these progress problems?
The answer lies in the fair choice assumption underlying the progress test. This means that every possible execution of the system will eventually happen including those in which cars do not starve. To detect progress problems we must superimpose some scheduling policy for actions, which models the situation in which the bridge is heavily used, i.e. we need to impose adverse conditions which "stress-test" the system. We use action priority expressions to describe these scheduling policies. Action priority is specified in FSP with respect to process compositions.
||C = (P||Q) ≪{a1,...,an}
specifies a composition in which the actions a1,...,an
have higher priority than any other action in the alphabet of P||Q
including the silent action tau
. In any choice in this system which has one or more of the actions a1,...,an
labeling a transition, the transitions labeled with lower priority actions are discarded.
||C = (P||Q)≫{a1,...,an}
specifies a composition in which the actions a1,...,an
have lower priority than any other action in the alphabet of P||Q
including the silent action tau
. In any choice in this system which has one or more transitions not labeled by a1,...,an
, the transitions labeled by a1,...,an
are discarded.
Action priority operators simplify the composite processes by discarding particular transitions. Figure 7.10 illustrates the effect for a simple example. When work
is specified to be a high priority action in the composition HIGH
, the sleep
transition disappears since it is lower priority and consequently in a choice between sleep
and work, work
will always be chosen. When work
is specified to be a low priority action in the composition LOW
, the work
transition disappears since it is lower priority and consequently in a choice between sleep
and work, sleep
will always be chosen.
Using progress properties and action priorities, we are now in a position to investigate the liveness problems of the single-lane bridge. In particular, we are interested in the following two progress properties when the bridge is heavily loaded or congested.
progress
BLUECROSS = {blue[ID].enter}progress
REDCROSS = {red[ID].enter}
BLUECROSS
asserts that it is always the case that one of the blue cars will be able to enter the bridge; REDCROSS
asserts the same for red cars. This leaves the problem of how to model congestion using action priority. If we give all the actions related to red cars priority over blue cars we get the situation where BLUECROSS
is violated and similarly if we give blue cars priority REDCROSS
is violated. Neither of these scheduling policies is a good model of the program. Neither red nor blue cars have priority in the implementation. Instead, we give car exit from the bridge low priority. This models the situation where the bridge is congested since in any choice between another car entering the bridge and a car leaving the bridge, we choose to let a car enter. The congested bridge is modeled by:
||CongestedBridge = (SingleLaneBridge) ≫{red[ID].exit,blue[ID].exit}.
Progress analysis of this system against the properties BLUECROSS
and REDCROSS
produces the following output:
Progress violation: BLUECROSS Path to terminal set of states: red.1.enter red.2.enter Actions in terminal set: {red.1.enter, red1.exit, red.2.enter, red.2.exit, red.3.enter, red.3.exit} Progress violation: REDCROSS Path to terminal set of states: blue.1.enter blue.2.enter Actions in terminal set: {blue.1.enter, blue.1.exit, blue.2.enter, blue.2.exit, blue.3.enter, blue.3.exit}
The output corresponds with observations of the program. When there are three cars and a red car enters first then the bridge is continuously occupied by red cars and blue cars never cross. Similarly, red cars never cross if a blue car enters first. However, the model abstracts from a number of program details such as the length of the bridge and consequently, the number of cars needed to continuously occupy it. As a result, the model detects lack of progress when there are only two cars moving in each direction. The terminal sets of states for this scenario can clearly be seen in the transition system depicted in Figure 7.11.
When there is only one car moving in each direction, the bridge does not become congested and both red and blue cars make progress. The transition system for the one car scenario is depicted in Figure 7.12.
Will we receive the same progress results if we instead model congestion by giving car entry to the bridge high priority? The interested reader should check that this is indeed the case.
What we must now do is devise a model which does not exhibit progress problems when there is more than one car moving in each direction.
A bridge which decides dynamically at any given point whether to admit blue cars or red cars needs to have more information about the state of cars than is currently available in the model. In particular, the bridge needs to know whether cars are waiting to cross. To this end, the model for a car is modified so that it requests access to the bridge before attempting to enter. The revised model for a car is:
CAR = (request->enter->exit->CAR).
The bridge model can now count the number of cars waiting at each end. The count is incremented when a car requests access and decremented when the car enters the bridge. Our first attempt at a revised BRIDGE
process uses this count of waiting cars as follows. Red cars are only allowed to enter the bridge if there are no blue cars on the bridge and there are no blue cars waiting. Blue cars are only allowed to enter the bridge if there are no red cars on the bridge and no red cars waiting to enter the bridge. The revised BRIDGE
process is as follows:
/* nr -number of red cars on the bridge
nb -number of blue cars on the bridge
wr -number of red cars waiting to enter
wb -number of blue cars waiting to enter
*/ BRIDGE = BRIDGE[0][0][0][0], BRIDGE[nr:T][nb:T][wr:T][wb:T] = (red[ID].request -> BRIDGE[nr][nb][wr+1][wb] |when
(nb==0 && wb==0) red[ID].enter -> BRIDGE[nr+1][nb][wr-1][wb] |red[ID].exit -> BRIDGE[nr-1][nb][wr][wb] |blue[ID].request -> BRIDGE[nr][nb][wr][wb+1] |when
(nr==0 && wr==0) blue[ID].enter -> BRIDGE[nr][nb+1][wr][wb-1] |blue[ID].exit -> BRIDGE[nr][nb-1][wr][wb] ).
The problem with this model is that when we check the safety properties of the new SingleLaneBridge
system, a deadlock is reported:
Trace to DEADLOCK: red.1.request red.2.request red.3.request blue.1.request blue.2.request blue.3.request
The trace is the scenario in which there are cars waiting at both ends, and consequently, the bridge does not allow either red or blue cars to enter. To solve this problem, we must introduce some asymmetry into the problem (as was done for the Dining Philosophers in Chapter 6). This takes the form of a boolean variable (bt
) which indicates whether it is the turn of blue cars or red cars to enter the bridge. Initially, bt
is set to true indicating it is blue's turn. As soon as a blue car exits the bridge, bt
is set to false. When a red car exits, bt
is set to true again. The BRIDGE
process becomes:
const
True = 1const
False = 0 range B = False..True /* nr -number of red cars on the bridge
nb -number of blue cars on the bridge
wr -number of red cars waiting to enter
wb -number of blue cars waiting to enter
bt -true indicates blue turn,
false indicates red turn
*/ BRIDGE = BRIDGE[0][0][0][0][True], BRIDGE[nr:T][nb:T][wr:T][wb:T][bt:B] = (red[ID].request ->BRIDGE[nr][nb][wr+1][wb][bt] |when
(nb==0 && (wb==0||!bt)) red[ID].enter ->BRIDGE[nr+1][nb][wr-1][wb][bt] |red[ID].exit ->BRIDGE[nr-1][nb][wr][wb][True] |blue[ID].request->BRIDGE[nr][nb][wr][wb+1][bt] |when
(nr==0 && (wr==0||bt)) blue[ID].enter ->BRIDGE[nr][nb+1][wr][wb-1][bt] |blue[ID].exit ->BRIDGE[nr][nb-1][wr][wb][False] ).
The condition under which the bridge permits a red car to enter is that there are no blue cars on the bridge and either there are no blue cars waiting or it is not blue's turn: nb==0 &&(wb==0 || !bt)
. The condition for a blue car to enter is that there are no red cars on the bridge and either there are no red cars waiting or it is blue's turn: nr==0 &&(wr==0 || bt)
.
This corrected model no longer deadlocks. Further, a progress analysis reports that BLUECROSS
and REDCROSS
properties are not violated.
The revision to the program involves a new version of the bridge monitor which implements precisely the BRIDGE
process from the model developed in the last section. In fact, we do not need to introduce a new monitor method to implement the request
action made by cars. The existing enter
methods can be modified to increment a wait count before testing whether or not the caller can access the bridge. As before, the tests are simply the negation of the guards in the model BRIDGE
process. The new implementation is listed in Program 7.5.
In the demonstration applet, this implementation of the monitor is used when the Fair check box is clicked.
Example 7.5. FairBridge
class.
class
FairBridgeextends
Bridge {private
int nred = 0;//count of red cars on the bridge
private
int nblue = 0;//count of blue cars on the bridge
private
int waitblue = 0;//count of waiting blue cars
private
int waitred = 0;//count of waiting red cars
private
boolean blueturn = true;synchronized
void redEnter()throws
InterruptedException { ++waitred;while
(nblue>0||(waitblue>0 && blueturn)) wait(); --waitred; ++nred; }synchronized
void redExit(){ --nred; blueturn = true;if
(nred==0)notifyAll(); }synchronized
void blueEnter(){throws
InterruptedException { ++waitblue;while
(nred>0||(waitred>0 && !blueturn)) wait(); --waitblue; ++nblue; }synchronized
void blueExit(){ --nblue; blueturn = false;if
(nblue==0) notifyAll(); } }
The Readers–Writers problem is concerned with access to a shared database by two kinds of processes. Readers execute transactions that examine the database while Writers both examine and update the database. For the database to be updated correctly, Writers must have exclusive access to the database while they are updating it. If no Writer is accessing the database, any number of Readers may concurrently access it. In this section, we develop a solution to the problem. As usual, we construct a model of the problem to examine its safety and liveness properties before proceeding to an implementation.
In modeling the problem, the first step is to decide on the actions of interest. These are acquiring and releasing read access to the shared database and acquiring and releasing write access. The actions are declared below as the set Actions
:
set
Actions = {acquireRead,releaseRead,
acquireWrite,releaseWrite}
As for the Ornamental Garden model in section 4.1.2, we use a set constant simply as a way of abbreviating the model description. The processes that model Readers and Writers are:
READER = (acquireRead->examine->releaseRead->READER) +Actions {examine}. WRITER = (acquireWrite->modify->releaseWrite->WRITER) +Actions {modify}.
A READER
process must acquire read access before examining the database and a WRITER
must acquire write access before modifying the database. The alphabets of both processes have been defined to be the full set of access actions by the alphabet extension +Actions
. This ensures that while a READER
only engages in the acquireRead
and releaseRead
actions, the acquireWrite
and releaseWrite
actions cannot occur freely for any prefixed instance of the process. Similarly, for WRITER
processes, the acquireRead
and releaseRead
actions cannot occur freely. The examine
and modify
actions are hidden since they are irrelevant to the problem of synchronizing access to the shared database.
Access to the shared database is controlled by a read/write lock. The lock accepts acquireRead
actions when it has not been acquired for write access by acquireWrite
. It permits only a single write access when it has not been acquired for read access. The lock is modeled by the RW_LOCK
process:
const
False = 0const
True = 1range
Bool = False..Trueconst
Nread = 2// Maximum readers
const
Nwrite= 2// Maximum writers
RW_LOCK = RW[0][False], RW[readers:0..Nread][writing:Bool] = (when
(!writing) acquireRead ->RW[readers+1][writing] |releaseRead ->RW[readers-1][writing] |when
(readers==0 && !writing) acquireWrite->RW[readers][True] |releaseWrite ->RW[readers][False] ).
The RW_LOCK
process maintains a count of the number of concurrent read accesses (readers
) and a boolean (writing
) which is set to true when the lock is acquired for write access. The action to acquire read access is only accepted when writing
is false and the action to acquire write access is only accepted when readers==0
and writing
is false.
To check that the lock behaves as desired, we define a safety property, RW_SAFE
, as follows:
property
SAFE_RW = (acquireRead->READING[1] |acquireWrite->WRITING ), READING[i:1..Nread] = (acquireRead->READING[i+1] |when
(i>1) releaseRead ->READING[i-1] |when
(i==1)releaseRead ->SAFE_RW ), WRITING = (releaseWrite->SAFE_RW).
The property asserts that initially either an acquireRead
action or a acquireWrite
action can be accepted. In other words when the lock is free, it can be acquired for either read or write access. When acquired for read access (READING
), further acquireRead
actions are permitted but no acquireWrite
actions. The lock does not become free until all the releaseRead
actions which correspond to the acquireRead
actions have happened. When the lock has been acquired for write access (WRITING
), only the releaseWrite
action should occur. To check that the lock implementation RW_LOCK
satisfies the property, the lock is composed with the property as follows:
||READWRITELOCK = (RW_LOCK || SAFE_RW).
The resulting LTS is depicted in Figure 7.13.
The transitions to the ERROR
state in Figure 7.13 occur if a Reader or Writer is badly behaved. For example, if a Reader performs a releaseRead
without previously having performed an acquireRead
then a safety violation will occur. A violation will also occur if more than two acquireRead
requests are made.
The composition of READER
and WRITER
processes with the read/write lock is described in Figure 7.14. Analysis of this system reveals no deadlocks or safety violations. The addition of well-behaved READER
and WRITER
processes ensures that the error transitions of Figure 7.13 cannot occur.
The progress properties that are important in the Readers–Writers system are that both Readers and Writers should eventually acquire access to the shared database. We can express the required progress properties as follows:
progress
WRITE = {writer[1..Nwrite].acquireWrite}progress
READ = {reader[1..Nread].acquireRead}
The WRITE
property asserts that it should always be the case that at least one of the WRITER
processes can perform an acquireWrite
action. Since WRITER
s are completely symmetric, we can reasonably expect that if one can acquireWrite
then so can the others. READ
specifies the same property for READER
processes and acquireRead
. A progress check reports no violations of these properties in the system specified by READERS_WRITERS
. Because of the fair choice assumption, progress problems only occur in complete system models that are erroneous. To find how the system performs when loaded or "stressed", we must specify adverse scheduling conditions using action priority. This is exactly the procedure we adopted to find the progress problems in the single-lane bridge model. Indeed, the adverse conditions are similar to those used in the bridge problem. To model a heavily loaded system, we give lower priority to release
actions in the same waywegavelowerpriorityto exit
actions in the bridge problem. (Alternatively, we could give higher priority to the acquire
actions.) The system model used for progress analysis is described by:
||RW_PROGRESS = READERS_WRITERS ≫{reader[1..Nread].releaseRead, writer[1..Nread].releaseWrite}.
Analysis of this system leads to the violation:
Progress violation: WRITE Path to terminal set of states: reader.1.acquireRead Actions in terminal set: {reader.1.acquireRead, reader.1.releaseRead, reader.2.acquireRead, reader.2.releaseRead}
The violation describes the scenario in which Writers cannot access the shared database because a Reader always has read access. In other words, the number of Readers never drops to zero and consequently, the read/write lock denies access to Writers. The terminal set of states that describes this behavior can clearly be seen in Figure 7.15. It contains the states numbered 3, 4 and 5. Before exploring solutions to this progress problem, we translate the existing model into an implementation in the next section.
In the interests of brevity, we describe only the monitor that synchronizes the accesses of Readers and Writers to a shared database. This synchronization is the essence of the problem. In the same way that we defined the set of actions of interest in the Readers–Writers model, we define an interface that identifies the monitor methods that must be implemented. In the sections that follow, we develop a number of alternative implementations of this interface. The interface is listed in Program 7.6.
Example 7.6. ReadWrite
interface.
interface
ReadWrite {public
void acquireRead()throws
InterruptedException;public
void releaseRead();public
void acquireWrite()throws
InterruptedException;public
void releaseWrite(); }
Each method in the ReadWrite
interface corresponds directly to the action of the same name in the model. Our first implementation of ReadWrite
, which corresponds exactly to the RW_LOCK
process from the model, is listed in Program 7.7.
The guarded actions from the model become synchronized methods containing waits. However, in the implementation, we must decide on notification to awake threads blocked in waits. The simple solution, as discussed in Chapter 5, is to include a call to notifyAll()
in every monitor method that modifies the state of the monitor. However, this can lead to unnecessary thread switches. In the ReadWriteSafe
monitor, notification is required only when the last Reader has relinquished access and when a Writer releases. When the last Reader calls releaseRead()
(i.e. readers==0
), notify()
rather than notifyAll()
can be used since only Writers can be waiting and it is only necessary to unblock a single Writer. When a Writer is finished it calls releaseWrite()
which then calls notifyAll()
. This is because it may be necessary to unblock either one or more Readers or a Writer.
Example 7.7. ReadWriteSafe
class.
class
ReadWriteSafeimplements
ReadWrite {private
int readers =0;private
boolean writing = false;public synchronized
void acquireRead()throws
InterruptedException {while
(writing) wait(); ++readers; } publicsynchronized
void releaseRead() { --readers;if
(readers==0) notify(); }
public synchronized
void acquireWrite()throws
InterruptedException {while
(readers>0 || writing) wait(); writing = true; }public synchronized
void releaseWrite() { writing = false; notifyAll(); } }
The implementation suffers from the progress problem detected in the model. If the number of Readers accessing the shared database never drops to zero, then Writers can never gain access. This behavior can be seen in the demonstration applet when the two Reader threads are started such that there is always one holding the lock. The applet display is depicted in Figure 7.16.
To address the progress problem discovered with our first model and implementation of the Readers–Writers problem, we adopt an approach in which Readers are denied access if there are Writers waiting to acquire access. This should give Writers priority in acquiring the lock and avoid the situation in which they wait forever for access. To detect that a Writer is waiting for access, we must add another action to its repertoire. A Writer must request access before attempting to acquire it. This is exactly the same solution we adopted in the single-lane bridge solution to detect whether cars were waiting. The additional action is requestWrite
and the revised WRITER
process is shown below:
set
Actions = {acquireRead,releaseRead,
acquireWrite,releaseWrite,requestWrite}
WRITER =
(requestWrite->acquireWrite->modify
->releaseWrite->WRITER
)+Actions{modify}.
The READER
process remains unchanged. RW_LOCK
is modified to maintain a count of waiting Writers (waitingW
). The count is incremented when a Writer requests access and decremented when it actually acquires access. Readers are only allowed to acquire the lock when the number of waiting Writers is zero. The revised lock process is listed below:
RW_LOCK = RW[0][False][0], RW[readers:0..Nread] [writing:Bool] [waitingW:0..Nwrite] = (when
(!writing && waitingW==0) acquireRead -> RW[readers+1][writing][waitingW] |releaseRead -> RW[readers-1][writing][waitingW] |when
(readers==0 && !writing) acquireWrite-> RW[readers][True][waitingW-1] |releaseWrite -> RW[readers][False][waitingW] |requestWrite -> RW[readers][writing][waitingW+1] ).
This definition of RW_LOCK
still satisfies the RW_SAFE
property. Note that we have not had to change the definition of the safety property. The request
action (requestWrite
) is not relevant to the safe operation of the lock and so does not appear in the alphabet of the safety property. Safety is determined only by the correct sequencing of acquire
and release
actions.
A progress analysis of RW_PROGRESS
now produces the output:
Progress violation: READ Path to terminal set of states: writer.1.requestWrite writer.2.requestWrite Actions in terminal set: {writer.1.requestWrite, writer.1.acquireWrite, writer.1.releaseWrite, writer.2.requestWrite, writer.2.acquireWrite, writer.2.releaseWrite}
We no longer have a violation of the WRITE
property, demonstrating that in this Writers priority system, Writers can always access the shared database. However, we now have a READ
progress violation. This occurs since, if there is always a Writer waiting to acquire the lock, then Readers will never gain access. However, in the practical application of read/write locks, the Writers priority solution is often satisfactory since there are usually many more read accesses to a database than write accesses. In addition, it may be important that Readers get the most up-to-date information. The implementation of a Writers priority lock is listed in Program 7.8. It follows directly from the revised definition of RW_LOCK
.
A version of the read/write lock that satisfies both the READ
and WRITE
properties involves the addition of a boolean which indicates whether it is the Readers' turn or the Writers' turn. Readers only defer to waiting Writers when it is not their turn to acquire the lock. This turn variable plays exactly the same role in the Readers–Writers problem as the turn variable in the single-lane bridge. The final version of the read/write lock model is listed below. The implementation is left as an exercise.
RW_LOCK = RW[0][False][0][False], RW[readers:0..Nread] [writing:Bool] [waitingW:0..Nwrite] [readersturn:Bool] = (when
(!writing && (waitingW==0||readersturn)) acquireRead ->RW[readers+1][writing][waitingW][readersturn] |releaseRead ->RW[readers-1][writing][waitingW][False] |when
(readers==0 && !writing) acquireWrite ->RW[readers][True][waitingW-1][readersturn] |releaseWrite ->RW[readers][False][waitingW][True] |requestWrite ->RW[readers][writing][waitingW+1][readersturn] ).
Example 7.8. ReadWritePriority
class.
class
ReadWritePriorityimplements
ReadWrite{private
int readers =0;private
boolean writing = false;private
int waitingW = 0;// no of waiting Writers.
public synchronized
void acquireRead()throws
InterruptedException {while
(writing || waitingW>0) wait(); ++readers; }public synchronized
void releaseRead() { --readers;if
(readers==0) notifyAll(); }public synchronized
void acquireWrite()throws
InterruptedException { ++waitingW;while
(readers>0 || writing) wait(); --waitingW; writing = true; }public synchronized
void releaseWrite() { writing = false; notifyAll(); } }
A safety property asserts that nothing bad happens during the execution of a program and a liveness property asserts that something good eventually happens. In this chapter, we described how FSP models can be checked for both safety and liveness properties.
A safety property is defined by a deterministic process property
P. This asserts that any trace including actions in the alphabet of P
is accepted by P
. When the property P
is composed with a system S
, traces of actions that are in the alphabet of S
and the alphabet of P
must also be valid traces of P
, otherwise the ERROR
state is reachable. Consequently, if safety analysis does not detect an ERROR
state, we know that the property holds for the system.
We defined a subset of liveness properties which we termed progress properties. A progress property is defined by a progress set of action labels. It asserts that in any infinite execution of a system, one of the actions in the progress set must happen infinitely often. In asserting progress, it is necessary to make some scheduling assumptions as to which transitions are chosen for execution. We assume fair choice for a set of transitions such that if the set is executed infinitely often, then every transition in the set will be executed infinitely often. To investigate the liveness problems that occurred in our example programs, we introduced a way of specifying action priority that let us superimpose a specific scheduling policy on fair choice. This enabled us to model adverse situations in which processes compete for scarce resources.
The example programs developed in this chapter had a fixed set of threads competing for a resource. In Chapter 9, we examine systems in which the size of the set varies as threads are dynamically created and terminated.
The terms safety and liveness applied to concurrent programs were first introduced by Lamport (1977). In this book, we have adopted a modeling approach to reasoning about concurrent programs and consequently a model-checking approach to verifying properties. As discussed at the end of Chapter 5, an alternative approach is to reason about safety properties of concurrent programs using assertions and invariants specified in predicate logic. The reader should consult Greg Andrews' book (1991) for an extensive exposition of this approach.
The mechanisms used in this chapter for checking safety and progress properties have been developed by the authors and their colleagues. The safety property technique is due to Cheung and Kramer (1999). Progress checking as described here is due to Giannakopoulou, Magee and Kramer (1999) and is a simplified form of a more general technique for checking that properties specified in Linear Temporal Logic (LTL) hold for a system. The property that our approach checks is that an action always eventually occurs. As an LTL formula, this is specified as □◊a, where means always and ◊ means eventually. The general technique involves translating the LTL formula into Büchi automata and then composing the Büchi automata with the target system and performing a connected component analysis of the resulting automata (Gribomont and Wolper, 1989). Technically, this is a check that the system is a valid model of the formula – the origin of the term model checking, which we have used in the looser sense to refer to any technique for analyzing models. Büchi automata are a form of finite state automata which recognize infinite sequences of actions. The interested reader should look at Holzmann's SPIN model checker (Holzmann, 1991, 1997) which uses this approach. The pioneering work on model checking is due to Clarke, Emerson and Sistla (1986). Fred Schneider (1997) provides an introduction to temporal logic and discusses derivation and reasoning about concurrent programs.
Chapter 14 provides more details on the use of LTL for property checking, and introduces fluents to FSP as a means of specifying state-based properties in an event-based formalism such as LTS. Fairness and the use of Büchi automata are also discussed and supported by LTSA for property checking.
The topic of fairness in concurrent programs has an extensive literature. We have used a strong form of fair choice. For an extensive discussion on the different classes of fairness, see the book by Francez (1986).
7.1 What action trace violates the following safety property?
property
PS = (a->(b->PS|a->PS)|b->a->PS).
7.2 A lift has a maximum capacity of ten people. In the model of the lift control system, passengers entering a lift are signaled by an enter
action and passengers leaving the lift are signaled by an exit
action. Specify a safety property in FSP which when composed with the lift will check that the system never allows the lift that it controls to have more than ten occupants.
7.3 Specify a safety property for the car park problem of Chapter 5, which asserts that the car park does not overflow. Specify a progress property which asserts that cars eventually enter the car park. If car departure is lower priority than car arrival, does starvation occur?
7.4 In an operating system, a binary semaphore is used to control access to the console. The console is used by user processes and system processes. Construct a model of this system and investigate the scheduling conditions under which user processes may be denied access to the console.
7.5 Implement the system modeled in exercise 7.4 in Java using the ThreadPanel
and NumberCanvas
classes for display. Can you induce starvation in a user thread by giving it a lower scheduling priority using Thread.setPriority()
? If not, can you explain why starvation does not occur?
7.6 Two warring neighbors are separated by a field with wild berries. They agree to permit each other to enter the field to pick berries, but also need to ensure that only one of them is ever in the field at a time. After negotiation, they agree to the following protocol.
When one neighbor wants to enter the field, he raises a flag. If he sees his neighbor's flag, he does not enter but lowers his flag and tries again. If he does not see his neighbor's flag, he enters the field and picks berries. He lowers his flag after leaving the field.
Model this algorithm for two neighbors, n1
and n2
. Specify the required safety property for the field and check that it does indeed ensure mutually exclusive access. Specify the required progress properties for the neighbors such that they both get to pick berries given a fair scheduling strategy. Are there any adverse circumstances in which neighbors would not make progress? What if the neighbors are greedy?
(Hint: The following FSP can be used to model the flags.)
const
True = 1const
False = 0range
Bool = False..Trueset
BoolActions = {setTrue,setFalse,[False],[True]} BOOLVAR = VAL[False], VAL[v:Bool] = ( setTrue -> VAL[True] | setFalse -> VAL[False] | [v] -> VAL[v] ). ||FLAGS = (flag1:BOOLVAR||flag2:BOOLVAR).
7.7 Peterson's Algorithm for two processes (Peterson, G.L., 1981)
Fortunately for the neighbors in exercise 7.6, Gary Peterson visits one day and explains his algorithm to them. He explains that, in addition to the flags, the neighbors must share a turn indicator which can take the values 1 or 2. This is used to avoid potential deadlock.
When one neighbor wants to enter the field, he raises his flag and sets the turn indicator to indicate his neighbor. If he sees his neighbor's flag and it is his neighbor's turn, he may not enter but must try again later. Otherwise, he can enter the field and pick berries and must lower his flag after leaving the field.
For instance, neighbor n1
behaves as shown below, and neighbor n2
behaves symmetrically.
while
(true) { flag1 = true; turn = 2;while
(flag2 and turn==2) {}; enterField; pickBerries; flag1 = false; }
Model Peterson's Algorithm for the two neighbors. Check that it does indeed avoid deadlock and satisfy the mutual exclusion (safety) and berry-picking (progress) properties.
(Hint: The following FSP can be used to model the turn indicator.)
set
CardActions = {set1,set2,[1],[2]}
CARDVAR = VAL[1],
VAL[i:Card] = ( set1 -> VAL[1]
| set2 -> VAL[2]
| [i] -> VAL[i]
).
7.8 Implement Peterson's Algorithm as modeled in exercise 7.7.
3.145.61.170