Chapter 6, Process Synchronization

316
Chapter 6, Process Synchronization 1

description

Chapter 6, Process Synchronization. Cooperating processes can affect each other This may result from message passing It may result from shared memory space The general case involves concurrent access to shared resources. 6.1 Background. - PowerPoint PPT Presentation

Transcript of Chapter 6, Process Synchronization

Page 1: Chapter 6, Process Synchronization

1

Chapter 6, Process Synchronization

Page 2: Chapter 6, Process Synchronization

2

• Cooperating processes can affect each other• This may result from message passing• It may result from shared memory space• The general case involves concurrent access to

shared resources

Page 3: Chapter 6, Process Synchronization

3

6.1 Background

• This section illustrates how uncontrolled access to a shared resource can result in inconsistent state

• The following overheads show:– How producer and consumer threads may both

change the value of a variable, count– How the single increment or decrement of count is

not atomic in machine code– How the interleaving of machine instructions can give

an incorrect result

Page 4: Chapter 6, Process Synchronization

4

High level Producer Code

• while(count == BUFFER_SIZE)• ; // no-op• ++count;• buffer[in] = item;• in = (in + 1) % BUFFER_SIZE

Page 5: Chapter 6, Process Synchronization

5

High Level Consumer Code

• while(count == 0)• ; // no-op• --count;• Item = buffer[out];• out = (out + 1) % BUFFER_SIZE

Page 6: Chapter 6, Process Synchronization

6

Machine Code for Incrementing count

• register1 = count;• register1 = register1 + 1;• count = register1;

Page 7: Chapter 6, Process Synchronization

7

Machine Code for Decrementing count

• register2 = count;• register2 = register2 - 1;• count = register2;

Page 8: Chapter 6, Process Synchronization

8

An Interleaving of Machine Instructions Which Leads to a Lost Increment

• Let the initial value of count be 5• S0: Producer executes• register1 = count ( register1 = 5)• S1: Producer executes • register1 = register1 + 1 ( register1 = 6)• Context switch• S2: Consumer executes • register2 = count ( register2 = 5)• S3: Consumer executes • register2 = register2 – 1 ( register2 = 4)• Context switch• S4: producer executes • count = register1 ( count = 6)• Context switch• S5: consumer executes • count = register2 ( final value of count = 4)

Page 9: Chapter 6, Process Synchronization

9

• The point is that you started with a value of 5.• Then two processes ran concurrently.• One attempted increment the count.• The other attempted to decrement the count.• 5 + 1 – 1 should = 5• However, due to synchronization problems the

final value of count was 4, not 5.

Page 10: Chapter 6, Process Synchronization

10

• Term: • Race Condition. • Definition: • This is the general O/S term for any situation

where the order of execution of various actions affects the outcome (i.e., can result in an inconsistent state)

Page 11: Chapter 6, Process Synchronization

11

• The derivation of the term “race” condition: • Execution is a “race”. • In interleaved actions, whichever sequence

finishes first determines the final outcome. • Note in the concrete example that the one

that finished first “lost”.

Page 12: Chapter 6, Process Synchronization

12

• Process synchronization refers to the tools that can be used with cooperating processes to make sure that that during concurrent execution they access shared resources in such a way that a consistent state results

• In other words, it’s a way of enforcing a desired interleaving of actions, or preventing an undesired interleaving of actions.

Page 13: Chapter 6, Process Synchronization

13

• Yet another way to think about is that process synchronization reduces concurrency somewhat, because certain sequences that might otherwise happen are not allowed, and at least a partial sequential ordering of actions may be required

Page 14: Chapter 6, Process Synchronization

14

6.2 Critical Section Problem

• Term: • Critical section. • Definition: • A segment of code where resources common to

a set of threads are being manipulated• Note that the definition is given in terms of

threads because it will be possible to concretely illustrate it using threaded Java code

Page 15: Chapter 6, Process Synchronization

15

• Alternative definition: • A segment of code where access is regulated. • Only one thread at a time is allowed to

execute in the critical section • This makes it possible to avoid conflicting

actions which result in inconsistent state

Page 16: Chapter 6, Process Synchronization

16

• Critical section definition using processes: • Let there be n processes, P0, …, Pn-1, that share

access to common variables, data structures, or resources

• Any segments of code where they access shared resources are critical sections

• No two processes can be executing in their critical section at the same time

Page 17: Chapter 6, Process Synchronization

17

• The critical section problem is to design a protocol that allows processes to cooperate

• In other words, it allows them to run concurrently, but it prevents breaking their critical sections into parts and interleaving them

• Once again recall that this will ultimately be illustrated using threads

• In that situation, no two threads may be executing in the same critical section of code at the same time

Page 18: Chapter 6, Process Synchronization

18

• For the purposes of thinking about the problem, the general structure of a process with a critical section can be diagrammed in this way:

• while(true)• {• entry section // the synchronization

entrance protocol is implemented here• critical section // this section is

protected• exit section // the synchronization exit

protocol is implemented here• remainder section // this section is not

protected• }

Page 19: Chapter 6, Process Synchronization

19

• Note the terminology:• Entry section• Critical section• Exit section• Remainder section• These terms for referring to the parts of a

concurrent process will be used in the discussions which follow

Page 20: Chapter 6, Process Synchronization

20

• A correct solution to the critical section problem has to meet these three conditions:– Mutual exclusion– Progress– Bounded waiting

• In other words, an implementation of a synchronization protocol has to have these three characteristics in order to be correct.

Page 21: Chapter 6, Process Synchronization

21

Mutual exclusion

• Definition of mutual exclusion:• If process Pi is executing in its critical section, no

other process can be executing in its critical section– Mutual exclusion is the heart of concurrency control.– However, concurrency control is not correct if the

protocol “locks up” and the program can’t produce results.

– That’s what the following requirements are about.

Page 22: Chapter 6, Process Synchronization

22

Progress

• Definition of progress:• If no process is in its critical section and some

processes wish to enter, only those not executing in their remainder sections can participate in the decision

Page 23: Chapter 6, Process Synchronization

23

Progress explained

• For the sake of discussion, let all processes be structured as infinite loops

• Then a process can either be in its critical section or its remainder section

• In a system where mutual exclusion has been implemented, it may also be at the top of the loop, waiting for the entry section to allow it into the critical section

Page 24: Chapter 6, Process Synchronization

24

• The premise of the progress condition is that no process is in its critical section

• Some processes may be in their remainder sections

• Others may be waiting to enter the critical section

Page 25: Chapter 6, Process Synchronization

25

• Progress states that a process that is happily running in its remainder section has no part in the decision of which process to allow into the critical section.

• A process in the remainder section can’t stop another process from entering the critical section.

• A process in the remainder section also cannot delay the decision.

• The decision on which process enters can only take into account those processes that are currently waiting to get in

Page 26: Chapter 6, Process Synchronization

26

Bounded waiting

• Definition of bounded waiting:• There exists a bound, or limit, on the number

of times that other processes are allowed to enter their critical sections after a given process has made a request to enter its critical section and when that request is granted

Page 27: Chapter 6, Process Synchronization

27

Bounded waiting explained

• Whatever algorithm or protocol is implemented, it cannot allow starvation

• Granting access to a critical section is reminiscent of scheduling.

• Eventually, everybody has to get a chance

Page 28: Chapter 6, Process Synchronization

28

• For the purposes of this discussion, it is assumed that each process is executing at non-zero speed, although they may differ in their speeds

• Bounded waiting is expressed in terms of “a number of times”.

• No concrete time limit can be given, but the result is that allowing a thread into its critical section can’t be postponed indefinitely

Page 29: Chapter 6, Process Synchronization

29

More about the critical section problem

• The critical section problem is unavoidable in operating systems

• This idea already came up in chapter 5 in the discussion of preemption and interrupt handling

Page 30: Chapter 6, Process Synchronization

30

• You might try to avoid the critical section problem by disallowing cooperation among user processes (although this diminishes the usefulness of multi-programming)

• Such a solution would be very limiting for application code.

• It doesn’t work for system code.

Page 31: Chapter 6, Process Synchronization

31

• By definition, multiple system processes contend for shared resources like scheduling queues, I/O queues, lists of memory allocation, lists of processes, etc.

• The code that manipulates these resources has to be in a critical section

• Stated briefly: There has to be mutual exclusion between different processes that access the resources

Page 32: Chapter 6, Process Synchronization

32

The critical section problem in the O/S, elaborated

• You might try to get rid of the critical section problem by making the kernel monolithic (although this goes against the grain of layering/modular design)

• Even so, if the architecture is based on interrupts, whether the O/S is modular or not, one activation of the O/S can be interrupted and set aside, while another activation is started as a result of the interrupt.

Page 33: Chapter 6, Process Synchronization

33

• The idea behind “activations” here, can be illustrated with interrupt handling code specifically.

• One activation of the O/S may be doing one thing.

• When the interrupt arrives a second activation occurs, which will run a different part of the O/S—namely an interrupt handler

Page 34: Chapter 6, Process Synchronization

34

• There is only one ready queue, for example, in the O/S.

• Any activation of the O/S has the potential to access and modify this shared resource

• This would be inescapably true if the O/S were monolithic, but it’s no less true that different modules would affect shared resources

Page 35: Chapter 6, Process Synchronization

35

• The situation can be framed in this way: You think of the O/S as “owning” and “managing” the processes, whether system or user processes

• It turns out that in a sense, the processes own the O/S.

• The O/S may create the processes, but the processes can then be viewed as having shared access to common O/S resources

Page 36: Chapter 6, Process Synchronization

36

• This thought can be pursued one level deeper.• Even if the processes are user processes and

don’t access O/S resources directly, user requests for service and the granting of those requests by the O/S causes changes to the O/S’s data structures

Page 37: Chapter 6, Process Synchronization

37

• This is the micro-level view of the idea expressed at the beginning of the course, that the O/S is the quintessential service program

• Everything that it does can ultimately be traced to some application request

• Applications don’t own the system resources, but they are responsible for O/S behavior which requires critical section protection

Page 38: Chapter 6, Process Synchronization

38

Another way of thinking about this

• As soon as multiple processes are allowed, whether an O/S is a microkernel or not, it is reasonable to implement some of the functionality in different processes

• At that point, whether the O/S supported multi-programming or not, the O/S itself already has concurrency issues

• Once you take the step of allowing >1 concurrent process, whether user or system processes, concurrency, or the critical section problem arises

Page 39: Chapter 6, Process Synchronization

39

What all this means to O/S code

• O/S code can’t allow race conditions to arise• The O/S can’t be allowed to enter an

inconsistent state• O/S code has to be written so that access to

shared resources is done in critical sections• No two O/S processes can be in a critical

section at the same time

Page 40: Chapter 6, Process Synchronization

40

• The critical section problem has to be solved in order to implement a correct O/S.

• If the critical section problem can be resolved in O/S code, then the solution tools can also be used when considering user processes or application code which exhibit the characteristics of concurrency and access to shared resources

Page 41: Chapter 6, Process Synchronization

41

Dealing with critical sections in the O/S

• Keep in mind:– A critical section is a sequence of instructions that

has to be run atomically without interleaving executions of >1 process

– Pre-emptive scheduling a new process can be scheduled before the currently running one finishes

Page 42: Chapter 6, Process Synchronization

42

• The question of critical sections in the O/S intersects with the topic of scheduling

• The question of scheduling leads to two possible approaches to dealing with concurrency in the O/S:– A non-preemptive kernel– A preemptive kernel

Page 43: Chapter 6, Process Synchronization

43

• In a non-preemptive kernel, any kernel mode process will run until:– It exits– It blocks due to making an I/O request– It voluntarily yields the CPU

• Under this scenario, the process will run through any of its critical sections without interruption by another process

Page 44: Chapter 6, Process Synchronization

44

• A preemptive kernel may be desirable.• A preemptive kernel– Will be more responsive for interactive time-

sharing– Will support (soft) real-time processing

Page 45: Chapter 6, Process Synchronization

45

• Writing a pre-emptive kernel means dealing with concurrency in the kernel code– In general, this is not easy– In some cases (like SMP) it is virtually impossible

(how do you control two threads running concurrently on two different CPU’s?)

Page 46: Chapter 6, Process Synchronization

46

• Windows XP and before were non-preemptive• Traditional Unix was non-preemptive. • Newer versions of Unix and Linux are

preemptive

Page 47: Chapter 6, Process Synchronization

47

Returning to a discussion of what the critical section problem is, and how it can be solved, in general

• Explaining the requirements for a successful solution to the critical section problem– Mutual exclusion– Progress– Bounded waiting

Page 48: Chapter 6, Process Synchronization

48

• At this point, let it be assumed that the mutual exclusion requirement is clear

• Progress and bounded waiting can be made clearer by considering possible solutions to the critical section problem that may not satisfy these requirements

Page 49: Chapter 6, Process Synchronization

49

• Note that the latest edition of the textbook has taken out some of this material

• I have kept it because I think it is useful• The basic ideas come from earlier editions of

the book• The explanations of the ideas are unique to

this set of overheads

Page 50: Chapter 6, Process Synchronization

50

Scenario 1

• Suppose your protocol is based on the idea that processes “take turns”

• Consider two processes, P0 and P1

• Let “whose turn” be represented by an integer variable turn which takes on the values 0 or 1

Page 51: Chapter 6, Process Synchronization

51

• Note that regardless of whose turn it currently is, you can always change turns by setting turn = turn - 1

• If turn == 0, then 1 – turn = 1 gives the opposite• If turn == 1, then 1 – turn = 0 gives the opposite• On the other hand, in code protected by an if

statement, if(turn == x) you can simply hard code the change to turn = y, the opposite of x.

Page 52: Chapter 6, Process Synchronization

52

• Let the sections of the problem be structured in this way:

• while(true)• {• entry: If it’s my turn• critical section• exit: Change from my turn to the other• remainder• }

Page 53: Chapter 6, Process Synchronization

53

• For the sake of discussion assume:– P0 and P1 are user processes– The system correctly (atomically) grants access to

the shared variable turn• The code blocks for the two processes could

be spelled out in detail as given below

Page 54: Chapter 6, Process Synchronization

54

P0

• while(true)• {• entry: if(turn == 0)• {• critical section• exit: turn = 1;• }• remainder• }

Page 55: Chapter 6, Process Synchronization

55

P1

• while(true)• {• entry: if(turn == 1)• {• critical section• exit: turn = 0;• }• remainder• }

Page 56: Chapter 6, Process Synchronization

56

• Suppose that turn is initialized arbitrarily to either 1 or 0

• Assume that execution of the two processes goes from there

• This would-be solution violates progress

Page 57: Chapter 6, Process Synchronization

57

• Consider this scenario:• turn == 0• P0 runs its critical section• It then goes to its exit section and sets turn to

1• P1 is in its remainder section and doesn’t want

to enter its critical section (yet)

Page 58: Chapter 6, Process Synchronization

58

• P0 completes its remainder section and wants to enter its critical section again

• It can’t, because it effectively, and unnecessarily locked itself out

• Due to the structure of the code, only P1 can let it back in again.

Page 59: Chapter 6, Process Synchronization

59

• This scenario meets the requirements for a violation of the progress condition for a correct implementation of synchronization

• The scheduling of P0, a process that is ready to enter its critical section, is dependent on a process, P1, which is running in its remainder section—is not running in its critical section

Page 60: Chapter 6, Process Synchronization

60

• Here is a physical analogy• There is a single train track between two

endpoints• Access to the track is managed with a token• If the token is at point A, a train can enter the

track from that direction, taking the token with it• If the token is at point B, a train can enter the track

from that direction, taking the token with it

Page 61: Chapter 6, Process Synchronization

61

Page 62: Chapter 6, Process Synchronization

62

• This protocol works to prevent train wrecks• The need to hold the token forces strict

alternation between trains on the track• However, no two trains in a row can originate

from the same point, even if no train wants to go in the other direction

• This decreases the potential use of the tracks

Page 63: Chapter 6, Process Synchronization

63

Scenario 2

• Suppose that the same basic assumptions apply as for scenario 1

• There are two user processes with entry, critical, exit, and remainder sections

• You want to correctly synchronize them• You want to avoid the progress violation that

occurred under scenario 1

Page 64: Chapter 6, Process Synchronization

64

• Suppose that you think you’ll solve the problem of scenario 1 in this way:– Instead of rigidly assigning a turn using a single

variable, you’ll have two variables, one for each process

– The variables are used for a process to assert its desire to enter its critical section

– Then each process can defer to the other if the other wants to go, and whose turn it is will be determined accordingly

Page 65: Chapter 6, Process Synchronization

65

• For the sake of discussion, assume:– P0 and P1 are user processes– There are two, shared flag variables which are

boolean.– The system correctly (atomically) grants access to

the two shared variables

Page 66: Chapter 6, Process Synchronization

66

• Let the variables be named as follows, and let them represent the fact that a process wants to take a turn– flag0 == true P0 wants to enter its critical section

– flag1 == true P1 wants to enter its critical section

Page 67: Chapter 6, Process Synchronization

67

• Let the sections of the problem be structured in this way:

• while(true)• {• entry: Set my flag to true• while the other flag is true, wait• critical section• exit: Change my flag to false• remainder• }

Page 68: Chapter 6, Process Synchronization

68

• The given solution overcomes the problem of the previous scenario

• The processes don’t have to rigidly take turns to enter their critical sections

• If one process is in its remainder section, its flag will be set to false

• That means the other process will be free to enter its critical section

• A process not in its critical section can’t prevent another process from entering its critical section

Page 69: Chapter 6, Process Synchronization

69

• However, this new solution is still not fully correct.

• Recall that outside of the critical section, scheduling is determined independent of the processes

• That is to say that under concurrent execution, outside of a protected critical section, any execution order may occur

Page 70: Chapter 6, Process Synchronization

70

• So, consider the possible sequence of execution shown in the following diagram

• The horizontal arrow represents the fact that P0 is context switched out and P1 is context switched in

Page 71: Chapter 6, Process Synchronization

71

Page 72: Chapter 6, Process Synchronization

72

• Under this scheduling order, both processes have set their flags to true

• The result is called deadlock• Both processes will wait eternally for the other to

change its flag• This protocol is coded in such a way that the

decision to schedule is postponed indefinitely• This seems to violate the 2nd part of the definition

of progress

Page 73: Chapter 6, Process Synchronization

73

• The definition of the bounded waiting requirement on a correct implementation of synchronization states that there exists a bound, or limit, on the number of times that other processes are allowed to enter their critical sections after a given process has made a request to enter its critical section and when that request is granted

Page 74: Chapter 6, Process Synchronization

74

• Strictly speaking, scenario 2 doesn’t violate bounded waiting, because neither process can enter its critical section under deadlock.

• In effect, scenario 2 is another example of lack of progress.

• However, it is still interesting because it hints at the problems of unbounded waiting

Page 75: Chapter 6, Process Synchronization

75

• The picture on the following overhead illustrates what is wrong with scenario 2

• Each of the processes is too polite

Page 76: Chapter 6, Process Synchronization

76

Page 77: Chapter 6, Process Synchronization

77

6.3 Peterson’s Solution

• Keep in mind that in the previous two scenarios the assumption was made that the system was providing correct, atomic access to the turn and flag variables

• In essence, this assumption presumes a prior solution to the concurrency control problem

• In order to solve synchronize critical sections in code, we’re assuming that there is synchronized access to variables in the code

Page 78: Chapter 6, Process Synchronization

78

• For the sake of explaining the ideas involved, continue to assume that a system supports correct, atomic access to the needed variables

• Peterson’s solution is an illustration of synchronization in high level language software that would be dependent on atomicity in the machine language implementation to work

Page 79: Chapter 6, Process Synchronization

79

• Peterson’s solution is a combination of the previous two scenarios

• It uses both a turn variable and flag variables to arrive at a solution to the critical section problem that makes progress

• Let the solution have these variables– int turn– boolean flag[2]

Page 80: Chapter 6, Process Synchronization

80

• The meaning of the variables is as before• turn indicates which of the two processes is

allowed to enter the critical section• Instead of having two separate flag variables,

flag[] is given as a boolean array, dimensioned to size 2

• flag[] records whether either of the two processes want to enter its critical section

Page 81: Chapter 6, Process Synchronization

81

• Let the sections of the problem be structured in the way shown on the following overhead

• The code is given from the point of view of P1

• P2 would contain analogous but complementary code

Page 82: Chapter 6, Process Synchronization

82

• // Pi

• while(true)• {• flag[i] = true; // Assert that this process, i, wants to enter• • turn = j; // Politely defer the turn to the other process, j• • /* Note that the loop below does not enclose the critical section.• It is a busy waiting loop for process i. As long as its j’s turn• and j wants in the critical section, then i has to wait. The critical• point is that if it’s j’s turn, but j doesn’t want in, i doesn’t have• to wait. */• • while(flag[j] && turn == j);• • critical section;• • flag[i] = false;• • remainder section;• }

Page 83: Chapter 6, Process Synchronization

83

• Analyzing from the point of view of process Pi alone

• Let Pi reach the point where it wants to enter its critical section

• The following command signals its desire:• flag[i] = true;• It then politely does this in case Pj also wants to get

in:• turn = j;

Page 84: Chapter 6, Process Synchronization

84

• Pi only waits if turn == j and flag[j] is true

• If Pj doesn’t want in, then Pi is free to enter even though it set turn to j

• The argument is the converse if Pj wants in and Pi doesn’t want in

• In either of these two cases, there’s no conflict and the process gets to enter

Page 85: Chapter 6, Process Synchronization

85

• Consider the conflict case• Both Pi and Pj want in, so they both set their

flags• They both try to defer to each other by setting

turn to the other• Whichever one sets turn last defers to the

other and wins the politeness contest

Page 86: Chapter 6, Process Synchronization

86

• That means the other one goes first• As soon as it leaves the critical section, it

resets its flag• That releases the waiting process from the

busy loop

Page 87: Chapter 6, Process Synchronization

87

• Recall that outside of the critical section, scheduling is determined independent of the processes

• That is to say that under concurrent execution, outside of a protected critical section, any execution order may occur

• Let the timing sequence of execution be as shown in the diagram following the next overhead

Page 88: Chapter 6, Process Synchronization

88

• The horizontal arrow represents the fact that P0 is context switched out and P1 is context switched in

• In this diagram, P1 will win the politeness contest, meaning P0 will be allowed into the critical section when it is scheduled next

• It should be possible to trace the order of execution of the code blocks from this point on

Page 89: Chapter 6, Process Synchronization

89

Page 90: Chapter 6, Process Synchronization

90

• The book gives the explanation of Peterson’s solution in the form of a proof

• The demonstration here is not a proof• A non-proof understanding is sufficient• You should also be able to answer questions about

and sketch out Peterson’s solution• You should also be able to answer questions about

and sketch out the partial solutions of scenarios 1 and 2, which Peterson’s solution is based on

Page 91: Chapter 6, Process Synchronization

91

6.4 Synchronization Hardware

• The fundamental idea underlying the critical solution problem is locking

• while(true)• {• acquire lock• critical section• release lock• remainder section• }

Page 92: Chapter 6, Process Synchronization

92

• Locking means exclusive access—in other words, the ability to lock supports mutual exclusion

• In the previous explanations of synchronization, it was assumed that at the very least, variables could in essence be locked, and that made it possible to lock critical sections

• The question is, where does the ability to lock come from?

Page 93: Chapter 6, Process Synchronization

93

• Ultimately, locking, whether locking of a variable, a block of code, or a physical resource, can only be accomplished through hardware support

• There is no such thing as a pure software solution to locking

Page 94: Chapter 6, Process Synchronization

94

• At the bottom-most hardware level, one solution to locking/synchronization/critical section protection would be the following:

• Disallow interrupts and disable preemption• This was mentioned in passing early, and will be

covered again briefly• Keep in mind, though, that eventually a more

flexible construct, accessible to system code writers, needs to be made available

Page 95: Chapter 6, Process Synchronization

95

• Disallowing interrupts is a very deep topic• In reality, this means queuing interrupts• Interrupts may not be handled immediately,

but they can’t be discarded• Interrupt queuing may be supported in

hardware• Interrupt queuing may impinge on the very

“lowest” level of O/S software

Page 96: Chapter 6, Process Synchronization

96

• Disabling preemption• On the one hand, this is a simple solution to the

critical section/synchronization problem• On the other hand, it is a gross solution, which

severely diminishes concurrency• Whether they supported synchronization in user

processes, some older, simpler operating systems solved the synchronization problem in system code by disallowing preemption of system processes

Page 97: Chapter 6, Process Synchronization

97

• The overall problem with drastic solutions like uninterruptibility and non-preemption is loss of concurrency.

• Loss of concurrency reduces the effectiveness of time-sharing systems

• Loss of concurrency would have an especially large impact on a real time system

Page 98: Chapter 6, Process Synchronization

98

• Trying to make a given process uninterruptible in a multi-processor system is a mess.

• Each processor has to be sent a message saying that no code can be run which would conflict with a given kernel process

• Also, the system clock may be driven by interrupts.

• Delaying interrupts in such an architecture can slow the clock

Page 99: Chapter 6, Process Synchronization

99

A flexible, accessible locking construct

• Modern computer architectures have machine instructions which support locking

• These instructions have traditionally been known as test and set instructions

• The book refers to them as get and set instructions

Page 100: Chapter 6, Process Synchronization

100

• The book also illustrates these instructions with a Java like class with a private instance variable and public methods

• I prefer the traditional, straightforward, hardware/machine language explanation

Page 101: Chapter 6, Process Synchronization

101

• The fundamental problem of synchronization is mutual exclusion

• You need an instruction at the lowest level that either executes completely or doesn’t execute at all

• By definition, this is an atomic instruction

Page 102: Chapter 6, Process Synchronization

102

• In order to support locking, you need such an instruction which will both check the current value of a variable (for example), and depending on the value, set it to another value

• This has to be accomplished without the possibility that another process will have its execution interleaved with this one, affecting the outcome on that variable

Page 103: Chapter 6, Process Synchronization

103

• The name “test and set” indicates that this is an instruction with a composite action

• Even though it is composite, at the machine level, it is implemented as an atomic instruction

• Execution of a block of code containing it may be interrupted before the instruction is executed or after it is executed

• However, the instruction itself cannot be interrupted between the test and the set

Page 104: Chapter 6, Process Synchronization

104

• The test and set instruction can be diagrammed this way in high level pseudo-code

• if(current value of variable x is …)• set the value of x to …• The variable x itself now serves as the lock

Page 105: Chapter 6, Process Synchronization

105

• The entry into a critical section, the acquisition of the lock, takes this form

• if(x is not set) // These two lines together• set x; // are the atomic test and set• critical section;• unset x;• remainder section;

Page 106: Chapter 6, Process Synchronization

106

• The availability of this actual mutual exclusion at the machine language/hardware level is the basis for all correct/successful software solutions made available in API’s

• The book also mentions that an atomic swap instruction can be used to support mutual exclusion/locking.

• It is sufficient to understand the test and set concept

Page 107: Chapter 6, Process Synchronization

107

6.5 Semaphores

• An API may provide a semaphore construct as a synchronization tool for programmers

• A semaphore contains an integer variable and has two operations

• The first operation is acquire()• Historically this has been known as P()• P is short for “proberen”, which means “test”

in Dutch)

Page 108: Chapter 6, Process Synchronization

108

• The second operation is known as release()• Historically this has been known as V()• V is short for “verhogen”, which means

“increment” in Dutch

Page 109: Chapter 6, Process Synchronization

109

• On the following overheads high level language pseudo-code definitions are given for acquire() and release()

• The definitions are shown as multiple lines of code

• They would have to be implemented atomically in the API

Page 110: Chapter 6, Process Synchronization

110

• In these definitions there is a variable named value.

• The variable named value is essentially the lock variable

• In other words, at the system level mutual exclusion and synchronization have to be provided for on this variable

• Assume for starters that value is initialized to the value 1

Page 111: Chapter 6, Process Synchronization

111

acquire(), P()

• acquire()• {• while(value <= 0); // no-op• value--;• }

Page 112: Chapter 6, Process Synchronization

112

• Note that acquisition involves decrementing the variable value.

• Note also that this implementation is based on the idea of a waiting loop.

• Acquisition can’t occur if the value is less than or equal to zero.

• This should be reminiscent of elements of Peterson’s solution

Page 113: Chapter 6, Process Synchronization

113

release(), V()

• release()• {• value++;• }• Note that releasing involves incrementing the

variable value.• This is how it got its name in Dutch.• It is not conditional and it doesn’t involve any kind

of waiting.

Page 114: Chapter 6, Process Synchronization

114

Using Semaphores

• You may be mystified about what semaphores are for.

• The thing to keep in mind is that that they are a software construct that can be used to implement synchronization in user-level code.

• Internally, they themselves would have to be implemented in such a way that they relied on test and set instructions, for example, so that they really did synchronization

Page 115: Chapter 6, Process Synchronization

115

• We will not cover how you would implement semaphores internally

• Eventually, the closest we will come to synchronization in reality is using the Java syntax for it

• In the meantime, to explain synchronization further, the idea is to use semaphores as an example

Page 116: Chapter 6, Process Synchronization

116

• In multi-process or multi-threaded code, a single semaphore would be created to protect a single critical section

• Each process or thread would be given a reference to that common semaphore

• Semaphore S = new Semaphore();• Thread thread1 = new Thread(S);• Thread thread2 = new Thread(S);

Page 117: Chapter 6, Process Synchronization

117

• Then in the common code of the two threads, those threads would be prevented from entering the critical section at the same time by a set of calls like these:

• S.acquire();• // critical section• S.release();• // remainder section

Page 118: Chapter 6, Process Synchronization

118

• Along with the general problem of protecting a critical section, semaphores and locking can be used to enforce a particular order of execution of code when >1 process is running

• Suppose that the code for processes P1 and P2 are not exactly the same.

• P1 contains statement S1 and P2 contains statement S2, and it is necessary for S1 to be executed before S2

Page 119: Chapter 6, Process Synchronization

119

• Recall that in the introduction to semaphores, the semaphore was shown as initialized to the value 1.

• This is the “unlocked” value• If the semaphore is initialized to 0, then it is

initialized as locked

Page 120: Chapter 6, Process Synchronization

120

• Remember that even if the semaphore is locked, this doesn’t prevent code from running

• Code that doesn’t try to acquire the lock can run freely regardless of the semaphore value

• These considerations are important in the following example

• S1 and S2 can only be executed in 1-2 order if the code for P1 and P2 is written on the following overhead:

Page 121: Chapter 6, Process Synchronization

121

• Process P1 code• {• S1• semaphore.release();• }• Process P2 code• {• semaphore.acquire();• S2• }

Page 122: Chapter 6, Process Synchronization

122

• The foregoing example is not especially cosmic, but it does introduce an idea that will lead to more complicated examples later

• The key point is that two processes or threads are synchronized by means of a shared semaphore

• What is interesting is that the semaphore can be asymmetrically acquired and released in the different processes

Page 123: Chapter 6, Process Synchronization

123

• The next example doesn’t add anything new in particular, but the book reiterates the general idea with an example of part of a run() method for a Java thread

• No matter how many instances of the thread class are created, with a shared reference to a semaphore, only one at a time will be able to get into the critical section

Page 124: Chapter 6, Process Synchronization

124

• run()• {• …• while(true)• {• semaphore.acquire();• // critical section• semaphore.release();• // remainder section• }• }

Page 125: Chapter 6, Process Synchronization

125

Binary vs. Counting Semaphores

• A binary semaphore is a simple lock which can take on two values:– 1 = available (not locked)– 0 = not available (locked)

• A counting semaphore is initialized to an integer value n, greater than 1

Page 126: Chapter 6, Process Synchronization

126

• The initial value of a counting semaphore tells how many different instances of a given, interchangeable kind of resource there are

• The semaphore is still decremented by 1 for every acquisition and incremented by 1 for every release

• Although not used this way in practice, such a semaphore could also be used to allow n threads in a critical section at a time

Page 127: Chapter 6, Process Synchronization

127

Implementing Waiting in a Semaphore

• This was the implementation/definition of a semaphore given above

• acquire()• {• while(value <= 0); // no-op• value--;• }

Page 128: Chapter 6, Process Synchronization

128

• This is known as a spin lock• If the resource isn’t available, a process goes

into a busy waiting loop• It is known as busy waiting because the

process is still alive and will be scheduled• It will burn up its share of CPU time doing

nothing but spinning in the loop

Page 129: Chapter 6, Process Synchronization

129

• From the standpoint of the CPU as a resource, this is a waste

• The solution is to have a process voluntarily block itself when it can’t get a needed resource

• This is reminiscent of I/O blocking• The process should be put in a waiting list for

the resource to become available

Page 130: Chapter 6, Process Synchronization

130

• If the system supports a block() and a wakeup() call in the API, then semaphore definition can be structured in this way:

• acquire()• {• value--;• if(value < 0)• {• // Add this process to the waiting list.• block(); // Also, block the process that called• // acquire() on the semaphore. Details• // of how to do this are not shown.• }• }

Page 131: Chapter 6, Process Synchronization

131

• From a practical programming point of view, notice that the incrementing and decrementing occur before the “if”, not after the while, as in the original semaphore definition

• This works OK since the value is allowed to go negative

• If the count keeps track of waiting processes, it doesn’t make sense to do the decrementing only after successfully acquiring

Page 132: Chapter 6, Process Synchronization

132

• release()• {• value++;• if(value <= 0)• {• // Remove a process P• // from the waiting list.• wakeup(P);• }• }

Page 133: Chapter 6, Process Synchronization

133

• In general, in a counting semaphore, the value can become negative

• When it’s negative, its absolute value tells how many processes are waiting in the list for the resource

Page 134: Chapter 6, Process Synchronization

134

• In this latest iteration, locking by means of a semaphore no longer involves wasting CPU time on busy waiting.

• If such a semaphore can be implemented, and locking can be done in this way, then processes simply spend their idle time waiting in queues or waiting lists.

Page 135: Chapter 6, Process Synchronization

135

• How can a waiting list be implemented?• Essentially, just like an I/O waiting list• If this is a discussion of processes, then PCB’s are

entered into the list• There is no specific requirement on the order in

which processes are given the resource when it becomes available

• A FIFO queuing discipline ensures fairness and bounded waiting

Page 136: Chapter 6, Process Synchronization

136

• Note once again that semaphores are an explanatory tool—but they once again beg the question of mutual exclusion

• In other words, it’s obvious that the multi-value semaphore definitions of acquire() and release() consist of multiple lines of high level language code

• They will only work correctly if the entire methods are atomic

Page 137: Chapter 6, Process Synchronization

137

• This can be accomplished if the underlying system supports a synchronization mechanism that would allow mutual exclusion to be enforced from beginning to end of the semaphore methods

• In other words, the semaphore implementations themselves have to be critical sections

Page 138: Chapter 6, Process Synchronization

138

• In the implementation this might be enforced with the use of test and set type instructions

• At the system level, it again raises the question of inhibiting interrupts

Page 139: Chapter 6, Process Synchronization

139

Deadlocks and Starvation

• Although how it’s all implemented in practice might not yet be clear, the previous discussion of locking and semaphores illustrated the idea that you could enforce:– Mutual exclusion– Order of execution

• This should provide a basis for writing code where the end result is in a consistent state

Page 140: Chapter 6, Process Synchronization

140

• This still leaves two possible problems: Starvation and deadlock

• Starvation can occur for the same reasons it can occur in a scheduling algorithm

• If the waiting list for a given resource was not FIFO and used some priority for waking up blocked processes, some processes may never acquire the resource

• The simple solution to this problem is to implement a queuing discipline that can’t lead to starvation

Page 141: Chapter 6, Process Synchronization

141

• Deadlock is a more difficult problem• An initial example of this came up with scenario

2, presented previously• Deadlock typically arises when there are more

than one process and more than one resource they are contending for

• If the acquire() and release() calls are interleaved in a certain way, the result can be that neither process can proceed

Page 142: Chapter 6, Process Synchronization

142

Deadlock Example• Suppose that Q and S are resources and that P0 and P1 are

structured in this way:• P0 P1

• acquire(S) acquire(Q)• acquire(Q) acquire(S)• … …• release(S) release(Q)• release(Q) release(S)• Scheduling may proceed as follows• P0 acquires S• Then P1 acquires Q

Page 143: Chapter 6, Process Synchronization

143

• At this point, neither process can go any further

• Each is waiting for a resource that the other one holds

• This is a classic case of deadlock• This is a sufficiently broad topic that it will not

be pursued in depth here• A whole chapter is devoted to it later on

Page 144: Chapter 6, Process Synchronization

144

Classic Problems of Synchronization

• These problems exist in operating systems and other systems which have concurrency

• Because they are well-understood, they are often used to test implementations of concurrency control

• Some of these problems should sound familiar because the book has already brought them up as examples of aspects of operating systems (without yet discussing all of the details of a correct, concurrent implementation)

Page 145: Chapter 6, Process Synchronization

145

• The book discusses the following three problems– The bounded-buffer problem– The readers-writers problem– The dining philosophers problem

Page 146: Chapter 6, Process Synchronization

146

• The book gives Java code to solve these problems• For the purposes of the immediate discussion,

these examples are working code• There is one slight, possible source of confusion. • The examples use a home-made Semaphore class• In the current version of the Java API, there is a

Semaphore class

Page 147: Chapter 6, Process Synchronization

147

• Presumably the home-made class agrees with how the book describes semaphores

• It is not clear whether the API Semaphore class agrees or not

• The home-made class will be noted at the end of the presentation of code—but its contents will not be explained

• Only after covering the coming section on synchronization syntax in Java would it be possible to understand how the authors have implemented concurrency control in their own semaphore class

Page 148: Chapter 6, Process Synchronization

148

The Bounded Buffer Problem

• Operating systems implement general I/O using buffers and message passing between buffers

• Buffer management is a real element of O/S construction

• This is a shared resource problem• The buffer and any variables keeping track of

buffer state (such as the count of contents) have to be managed so that contending processes (threads) keep them consistent

Page 149: Chapter 6, Process Synchronization

149

• Various pieces of code were given in previous chapters for the bounded buffer problem

• Now the book gives code which is multi-threaded and also does concurrency control using a semaphore

• That code follows

Page 150: Chapter 6, Process Synchronization

150

• /**• * BoundedBuffer.java• *• * This program implements the bounded buffer with semaphores.• * Note that the use of count only serves to output whether• * the buffer is empty of full.• */

• import java.util.*;

• public class BoundedBuffer implements Buffer• {

• private static final int BUFFER_SIZE = 2;

• private Semaphore mutex;• private Semaphore empty;• private Semaphore full;

• private int count;• private int in, out;• private Object[] buffer;

Page 151: Chapter 6, Process Synchronization

151

• public BoundedBuffer()• {• // buffer is initially empty• count = 0;• in = 0;• out = 0;

• buffer = new Object[BUFFER_SIZE];

• mutex = new Semaphore(1);• empty = new Semaphore(BUFFER_SIZE);• full = new Semaphore(0);• }

Page 152: Chapter 6, Process Synchronization

152

• // producer calls this method• public void insert(Object item) {• empty.acquire();• mutex.acquire();

• // add an item to the buffer• ++count;• buffer[in] = item;• in = (in + 1) % BUFFER_SIZE;

• if (count == BUFFER_SIZE)• System.out.println("Producer Entered " + item

+ " Buffer FULL");• else• System.out.println("Producer Entered " + item

+ " Buffer Size = " + count);

• mutex.release();• full.release();• }

Page 153: Chapter 6, Process Synchronization

153

• // consumer calls this method• public Object remove() {• full.acquire();• mutex.acquire();

• // remove an item from the buffer• --count;• Object item = buffer[out];• out = (out + 1) % BUFFER_SIZE;

• if (count == 0)• System.out.println("Consumer Consumed " + item + " Buffer

EMPTY");• else• System.out.println("Consumer Consumed " + item + " Buffer

Size = " + count);

• mutex.release();• empty.release();

• return item;• }

• }

Page 154: Chapter 6, Process Synchronization

154

• There is more code to the full solution. • It will be given later, but the first thing to

notice is that there are three semaphores• The book has introduced a new level of

complexity “out of the blue” by using this classic problem as an illustration

Page 155: Chapter 6, Process Synchronization

155

• Not only is there a semaphore, mutex, for mutual exclusion on buffer operations

• There are two more semaphores, empty and full• These semaphores are associated with the idea that

the buffer has to be protected from trying to insert into a full buffer or remove from an empty one

• In other words, they deal with the concepts, given in an earlier chapter, of blocking sends/receives—writes/reads

Page 156: Chapter 6, Process Synchronization

156

• No cosmic theory is offered to explain the ordering of the calls to acquire and release the semaphores

• The example is simply given, and it’s up to us to try and sort out how the calls interact in a way that accomplishes the desired result

Page 157: Chapter 6, Process Synchronization

157

• The mutex semaphore is initialized to 1. • 1 and 0 are sufficient to enforce mutual

exclustion

Page 158: Chapter 6, Process Synchronization

158

• The empty semaphore is initialized to BUFFER_SIZE.

• The buffer is “empty”, i.e., has space to insert new items, until empty.acquire() has been called BUFFER_SIZE times and its space is filled

• In effect, the empty semaphore counts how many elements of the buffer array are empty and available for insertion

Page 159: Chapter 6, Process Synchronization

159

• The full semaphore is initialized to 0. • Initially, the buffer is not “full”. • There are no elements in the buffer array. • This means that remove() won’t find anything

until a call to insert() has made a call to full.release()

• In effect, the full semaphore counts how many elements of the buffer array have been filled and are available for removal

Page 160: Chapter 6, Process Synchronization

160

Page 161: Chapter 6, Process Synchronization

161

• In the code, the calls to acquire() and release() are paired in the insert() and release() methods

• The calls to acquire() and release() on empty and full are crossed between the insert() and remove() methods

• Notice that the pattern of criss-crossing of the calls is reminiscent of the trickery used in order to have a single semaphore control the order of execution of two blocks of code

Page 162: Chapter 6, Process Synchronization

162

• A semaphore is released after one block of code and acquired before the other

• This happens with both the empty and full semaphores in this example

• See the diagram on the following overhead

Page 163: Chapter 6, Process Synchronization

163

Page 164: Chapter 6, Process Synchronization

164

• The rest of the book code to make this a working example follows

Page 165: Chapter 6, Process Synchronization

165

• /**• * An interface for buffers• *• */

• public interface Buffer• {• /**• * insert an item into the Buffer.• * Note this may be either a blocking• * or non-blocking operation.• */• public abstract void insert(Object item);

• /**• * remove an item from the Buffer.• * Note this may be either a blocking• * or non-blocking operation.• */• public abstract Object remove();• }

Page 166: Chapter 6, Process Synchronization

166

• /**• * This is the producer thread for the bounded buffer problem.• */

• import java.util.*;

• public class Producer implements Runnable• {• public Producer(Buffer b) {• buffer = b;• }• • public void run()• {• Date message;• • while (true) {• System.out.println("Producer napping");• SleepUtilities.nap();• • // produce an item & enter it into the buffer• message = new Date(); • System.out.println("Producer produced " + message);• • buffer.insert(message);• }• }• • private Buffer buffer;• }

Page 167: Chapter 6, Process Synchronization

167

• /**• * This is the consumer thread for the bounded buffer problem.• */• import java.util.*;

• public class Consumer implements Runnable• {• public Consumer(Buffer b) { • buffer = b;• }• • public void run()• {• Date message;• • while (true)• {• System.out.println("Consumer napping");• SleepUtilities.nap(); • • // consume an item from the buffer• System.out.println("Consumer wants to consume.");• • message = (Date)buffer.remove();• }• }• • private Buffer buffer;• }

Page 168: Chapter 6, Process Synchronization

168

• /**• * This creates the buffer and the producer and consumer threads.• *• */• public class Factory• {• public static void main(String args[]) {• Buffer server = new BoundedBuffer();

• // now create the producer and consumer threads• Thread producerThread = new Thread(new Producer(server));• Thread consumerThread = new Thread(new Consumer(server));• • producerThread.start();• consumerThread.start(); • }• }

Page 169: Chapter 6, Process Synchronization

169

• /**• * Utilities for causing a thread to sleep.• * Note, we should be handling interrupted exceptions• * but choose not to do so for code clarity.• */

• public class SleepUtilities• {• /**• * Nap between zero and NAP_TIME seconds.• */• public static void nap() {• nap(NAP_TIME);• }

• /**• * Nap between zero and duration seconds.• */• public static void nap(int duration) {• int sleeptime = (int) (duration * Math.random() );• try { Thread.sleep(sleeptime*1000); }• catch (InterruptedException e) {}• }

• private static final int NAP_TIME = 5;• }

Page 170: Chapter 6, Process Synchronization

170

• The book’s Semaphore class follows• Strictly speaking, the example was written to use

this home-made class• Presumably the example would also work with

objects of the Java API Semaphore class• The keyword “synchronized” in the given class is

what makes it work• This keyword will be specifically covered in the

section of the notes covering Java synchronization

Page 171: Chapter 6, Process Synchronization

171

• /**• * Semaphore.java• *• * A basic counting semaphore using Java synchronization.• */

• public class Semaphore• {• private int value;

• public Semaphore(int value) {• this.value = value;• }

• public synchronized void acquire() {• while (value <= 0) {• try {• wait();• }• catch (InterruptedException e) { }• }

• value--;• }

• public synchronized void release() {• ++value;

• notify();• }• }

Page 172: Chapter 6, Process Synchronization

172

The Readers-Writers Problem

• The author explains this in general terms of a database

• The database is the resource shared by >1 thread

Page 173: Chapter 6, Process Synchronization

173

• At any given time the threads accessing a database may fall into two different categories, with different concurrency requirements– Readers: Reading is an innocuous activity– Writers: Writing (updating) is an activity which

changes the state of a database

Page 174: Chapter 6, Process Synchronization

174

• In database terminology, you control access to a data item by means of a lock

• If you own the lock, you have (potentially sole) access to the data item

• In order to implement user-level database locking, you need mutual exclusion on the code that accesses the lock

Page 175: Chapter 6, Process Synchronization

175

• This is just another one of my obscure side notes, like PCB’s as canopic jars…

• Maybe this will give you a better idea of what a lock is

• An analogy can be made with the title to a car and the car itself.

• If you possess the title, you own the car, allowing you to legally take possession of the car

Page 176: Chapter 6, Process Synchronization

176

• Database access adds a new twist to locking• There are two kinds of locks• An exclusive lock: This is the kind of lock

discussed so far. • A writer needs an exclusive lock which means

that all other writers and readers are excluded when the writer has the lock

Page 177: Chapter 6, Process Synchronization

177

• A shared lock: This is actually a new• This is the kind of lock that readers need. • The idea is that >1 reader can access the data at the

same time, as long as writers are excluded• The point is that readers don’t change the data, so

by themselves, they can’t cause concurrency control problems which are based on inconsistent state

• They can get in trouble if they are intermixed with operations that do change database state

Page 178: Chapter 6, Process Synchronization

178

• The book gives two different possible approaches to the readers-writers problem

• It should be noted that neither of the book’s approaches prevents starvation

• In other words, you might say that these solutions are application level implementations of synchronization which are not entirely correct, because they violate the bounded waiting condition

Page 179: Chapter 6, Process Synchronization

179

First Readers-Writers Approach

• No reader will be kept waiting unless a writer already has already acquired a lock

• Readers don’t wait on other readers• Readers don’t wait on waiting writers• Readers have priority• Writers may starve

Page 180: Chapter 6, Process Synchronization

180

Second Readers-Writers Approach

• Once a writer is ready, it gets the lock as soon as possible

• Writers have to wait for the current reader to finish, and no longer

• Writers have to wait on each other, presumably in FIFO order

• Writers have priority• Readers may starve

Page 181: Chapter 6, Process Synchronization

181

• Other observations about the readers-writers problem

• You have to be able to distinguish reader and writer threads (processes) from each other

• For this scheme to give much processing advantage, you probably need more readers than writers in order to justify implementing the different kinds of locks

• Garden variety databases would tend to have more readers than writers

Page 182: Chapter 6, Process Synchronization

182

• The solution approaches could be extended to prevent starvation and so that they also had other desirable characteristics

• Book code follows, along with some explanations• The first code solution takes approach 1: The

readers have priority• As a consequence, the provided solution would

allow starvation of writers to happen

Page 183: Chapter 6, Process Synchronization

183

• /**• * Database.java• *• * This class contains the methods the readers and writers will use• * to coordinate access to the database. Access is coordinated using

semaphores.• */

• public class Database implements RWLock• {• // the number of active readers• private int readerCount;

• Semaphore mutex; // controls access to readerCount• Semaphore db; // controls access to the database

• public Database() {• readerCount = 0;

• mutex = new Semaphore(1);• db = new Semaphore(1);• }

Page 184: Chapter 6, Process Synchronization

184

• public void acquireReadLock(int readerNum) {• mutex.acquire();

• ++readerCount;

• /* If I am the first reader tell all others that the database is being read. More to the point: Note that only if I am the first reader do I have to call acquire() on the db. Any other reader gets in without making the call to acquire(). */

• if (readerCount == 1)• db.acquire();

• System.out.println("Reader " + readerNum + " is reading. Reader count = " + readerCount);

• mutex.release();• }

Page 185: Chapter 6, Process Synchronization

185

• public void releaseReadLock(int readerNum) {• mutex.acquire();

• --readerCount;

• /* If I am the last reader tell all others (readers and writers) that the database is no longer being read. More to the point, only if I am the last reader do I make the call to release() the db. */

• if (readerCount == 0)• db.release();

• System.out.println("Reader " + readerNum + " is done reading. Reader count = " + readerCount);

• mutex.release();• }

Page 186: Chapter 6, Process Synchronization

186

• /* Note that unlike the read lock, the write lock acquisition and release are not conditional. */

• public void acquireWriteLock(int writerNum) {• db.acquire();• System.out.println("writer " + writerNum + " is writing.");• }

• public void releaseWriteLock(int writerNum) {• System.out.println("writer " + writerNum + " is done

writing.");• db.release();• }

• }

Page 187: Chapter 6, Process Synchronization

187

• The starting point for understanding this first database example is comparing it with the bounded-buffer example

• Like the bounded-buffer example, this example has more than one semaphore

• However, unlike the bounded-buffer example, it has two semaphores, not three, and the two are used to control different things

Page 188: Chapter 6, Process Synchronization

188

• An equally important difference from the bounded-buffer example is that this example does not actually do any reading or writing of data to a database

• The application code simply implements the protocol for assigning different kinds of locks to requesting processes

• It’s complicated enough as it is without trying to inject any reality into it.

• It’s sufficient to worry about the locking protocol

Page 189: Chapter 6, Process Synchronization

189

• Overall then, the major difference can be seen in the methods that are implemented

• In the bounded-buffer example, there were an insert() and a remove() method

• In this example there are four methods:– acquireReadLock()– acquireWriteLock()– releaseReadLock()– releaseWriteLock()

Page 190: Chapter 6, Process Synchronization

190

• Like with the bounded-buffer problem, understanding the acquisition and release of the read and write db locks depends on understanding the results of the placement of the various semaphore acquire() and release() calls in the code.

• Once again, you have to figure out what it means for the acquire() and release() of a semaphore lock to criss-cross (appear at the top/bottom or bottom/top) in the code for the db lock methods

Page 191: Chapter 6, Process Synchronization

191

• Observe that the write locks are pure and simple

• They enforce mutual exclusion on the database

• This is done with the db semaphore• See the code on the following overhead

Page 192: Chapter 6, Process Synchronization

192

• public void acquireWriteLock(int writerNum)• {• db.acquire();• System.out.println("writer " + writerNum + "

is writing.");• }

• public void releaseWriteLock(int writerNum) • {• System.out.println("writer " + writerNum + "

is done writing.");• db.release();• }

Page 193: Chapter 6, Process Synchronization

193

• The read locks make use of the mutex semaphore

• Mutex is a garden variety semaphore which enforces mutual exclusion on both the acquisition and release of read locks

• There is no fancy criss-crossing.

Page 194: Chapter 6, Process Synchronization

194

• Both acquireReadLock() and releaseReadLock() begin with mutex.acquire() and end with mutex.release()

• All of the db acquire and release code is protected, but in particular, the shared variable readerCount is protected

Page 195: Chapter 6, Process Synchronization

195

• The read locks also make use of the db semaphore

• This is the semaphore that protects the db• If a writer has already executed db.acquire(),

then a reader cannot get past the db.acquire() call in the acquireReadLock() method

Page 196: Chapter 6, Process Synchronization

196

• However, more than one reader can access the db at the same time

• Again, there is no criss-crossing• The call to db.acquire() occurs in

acquireReadLock()

Page 197: Chapter 6, Process Synchronization

197

• However, this call is conditional• Only the first reader has to make it• As long as no writer is holding the db, a reader

can enter the acquireReadLock() code, even if another reader has already acquired the db

Page 198: Chapter 6, Process Synchronization

198

• The call for a reader to release the db, db.release(), only occurs in releaseReadLock()

• However, this is also conditional• A read lock on the database is only released if

there are no more readers• The code is repeated below for reference

Page 199: Chapter 6, Process Synchronization

199

• public void acquireReadLock(int readerNum) {• mutex.acquire();

• ++readerCount;

• // if I am the first reader tell all others• // that the database is being read• if (readerCount == 1)• db.acquire();

• System.out.println("Reader " + readerNum + " is reading. Reader count = " + readerCount);

• mutex.release();• }

• public void releaseReadLock(int readerNum) {• mutex.acquire();

• --readerCount;

• // if I am the last reader tell all others• // that the database is no longer being read• if (readerCount == 0)• db.release();

• System.out.println("Reader " + readerNum + " is done reading. Reader count = " + readerCount);

• mutex.release();• }

Page 200: Chapter 6, Process Synchronization

200

• The rest of the book code to make this a working example follows

Page 201: Chapter 6, Process Synchronization

201

• /**• * An interface for reader-writer locks.• *• * In the text wedo not have readers and writers• * pass their number into each method. However we do so• * here to aid in output messages.• */

• public interface RWLock• {• public abstract void acquireReadLock(int readerNum);• public abstract void acquireWriteLock(int writerNum);• public abstract void releaseReadLock(int readerNum);• public abstract void releaseWriteLock(int writerNum);• }

Page 202: Chapter 6, Process Synchronization

202

• /**• * Reader.java• * A reader to the database.• */

• public class Reader implements Runnable• {

• private RWLock db;• private int readerNum;

• public Reader(int readerNum, RWLock db) {• this.readerNum = readerNum;• this.db = db;• }

• public void run() {• while (true) {• SleepUtilities.nap();

• System.out.println("reader " + readerNum + " wants to read.");• db.acquireReadLock(readerNum);

• // you have access to read from the database• // let's read for awhile .....• SleepUtilities.nap();

• db.releaseReadLock(readerNum);• }• }• }

Page 203: Chapter 6, Process Synchronization

203

• /**• * Writer.java• * A writer to the database.• */

• public class Writer implements Runnable• {• private RWLock server;• private int writerNum;

• public Writer(int w, RWLock db) {• writerNum = w;• server = db;• }

• public void run() {• while (true)• {• SleepUtilities.nap();

• System.out.println("writer " + writerNum + " wants to write.");• server.acquireWriteLock(writerNum);

• // you have access to write to the database• // write for awhile ...• SleepUtilities.nap();

• server.releaseWriteLock(writerNum);• }• }• }

Page 204: Chapter 6, Process Synchronization

204

• /**• * Factory.java• * This class creates the reader and writer threads and• * the database they will be using to coordinate access.• */

• public class Factory• {• public static final int NUM_OF_READERS = 3;• public static final int NUM_OF_WRITERS = 2;

• public static void main(String args[])• {• RWLock server = new Database();

• Thread[] readerArray = new Thread[NUM_OF_READERS];• Thread[] writerArray = new Thread[NUM_OF_WRITERS];

• for (int i = 0; i < NUM_OF_READERS; i++) {• readerArray[i] = new Thread(new Reader(i, server));• readerArray[i].start();• }

• for (int i = 0; i < NUM_OF_WRITERS; i++) {• writerArray[i] = new Thread(new Writer(i, server));• writerArray[i].start();• }• }• }

Page 205: Chapter 6, Process Synchronization

205

• /**• * Utilities for causing a thread to sleep.• * Note, we should be handling interrupted exceptions• * but choose not to do so for code clarity.• */

• public class SleepUtilities• {• /**• * Nap between zero and NAP_TIME seconds.• */• public static void nap() {• nap(NAP_TIME);• }

• /**• * Nap between zero and duration seconds.• */• public static void nap(int duration) {• int sleeptime = (int) (duration * Math.random() );• try { Thread.sleep(sleeptime*1000); }• catch (InterruptedException e) {}• }

• private static final int NAP_TIME = 5;• }

Page 206: Chapter 6, Process Synchronization

206

• These are the same observations that were made with the producer-consumer example– The book’s Semaphore class follows– Strictly speaking, the example was written to use this home-

made class– Presumably the example would also work with objects of

the Java API Semaphore class– The keyword “synchronized” in the given class is what

makes it work– This keyword will be specifically covered in the section of

the notes covering Java synchronization

Page 207: Chapter 6, Process Synchronization

207

• /**• * Semaphore.java• * A basic counting semaphore using Java synchronization.• */

• public class Semaphore• {• private int value;

• public Semaphore(int value) {• this.value = value;• }

• public synchronized void acquire() {• while (value <= 0) {• try {• wait();• }• catch (InterruptedException e) { }• }• value--;• }

• public synchronized void release() {• ++value;• notify();• }• }

Page 208: Chapter 6, Process Synchronization

208

The Dining Philosophers Problem

Page 209: Chapter 6, Process Synchronization

209

• Let there be one rice bowl in the center• Let there be five philosophers• Let there be only five chopsticks, one between

each of the philosophers

Page 210: Chapter 6, Process Synchronization

210

• Let concurrent eating have these conditions• 1. A philosopher tries to pick up the two

chopsticks immediately on each side– Picking up one chopstick is an independent act. – It isn’t possible to pick up both simultaneously.

Page 211: Chapter 6, Process Synchronization

211

• 2. If a philosopher succeeds in acquiring the two chopsticks, then the philosopher can eat. – Eating cannot be interrupted

• 3. When the philosopher is done eating, the chopsticks are put down one after the other– Putting down one chopstick is an independent act. – It isn’t possible to put down both simultaneously.

• Note that under these conditions it would not be possible for two neighboring philosophers to be eating at the same time

Page 212: Chapter 6, Process Synchronization

212

• This concurrency control problem has two challenges in it:

• 1. Starvation• Due to the sequence of events, one

philosopher may never be able to pick up two chopsticks and eat

Page 213: Chapter 6, Process Synchronization

213

• 2. Deadlock• Due to the sequence of events, each

philosopher may succeed in picking up either the chopstick on the left or the chopstick on the right. – None will eat because they are waiting/attempting

to pick up the other chopstick. – Since they won’t be eating, they’ll never finish and

put down the chopstick they do hold

Page 214: Chapter 6, Process Synchronization

214

• A full discussion of deadlock will be given in chapter 7– In the meantime, possible solutions to starvation and

deadlock under this scenario include:– Allow at most four philosophers at the table– Allow a philosopher to pick up chopsticks only if both

are available– An asymmetric solution: Odd philosophers reach first

with their left hands, even philosophers with their right

Page 215: Chapter 6, Process Synchronization

215

• Note that all proposed solutions either reduce concurrency or introduce artificial constraints

• The book gives partial code for this problem but having looked at all of the code for the previous two examples, it is not necessary to pursue more code for this one

Page 216: Chapter 6, Process Synchronization

216

6.7 Monitors

• Monitors are an important topic for two reasons– The are worth understanding because Java

synchronization is ultimately built on the monitor concept

– Also, the use of semaphores is fraught with difficulty, so overall, monitors might be a better concept to learn

Page 217: Chapter 6, Process Synchronization

217

• Recall that depending on the circumstance, correct implementation of synchronization using semaphores might require cross-over of calls

• The author points out that these are common mistakes when working with semaphores:

• 1. Reversal of calls: mutex.release() before mutex.acquire(). – This can lead to violation of mutual exclusion

Page 218: Chapter 6, Process Synchronization

218

• 2. Double acquisition: mutex.acquire() followed by mutex.acquire(). – This will lead to deadlock

• 3. Forgetting one or the other or both calls. – This will lead to a violation of mutual exclusion or

deadlock

Page 219: Chapter 6, Process Synchronization

219

• A high level, O-O description of what a monitor is:

• It’s a class with (private) instance variables and (public) methods

• Mutual exclusion is enforced over all of the methods at the same time

• No two threads can be in any of the methods at the same time

Page 220: Chapter 6, Process Synchronization

220

• Notice that this blanket mutual exclusion obviates calls to acquire() and release(), and therefore eliminates the problem of placing them correctly in calling code

• Notice also that under this scheme, the private instance variables are protected by definition

• There is no access to them except through the methods, and the methods have mutual exclusion enforced on them

Page 221: Chapter 6, Process Synchronization

221

The relationship of monitors to Java

• The discussion of monitors here is tangentially related to Java

• In Java there is a Monitor class, but that is something different from the monitor concept under discussion here

• However, it turns out that the Object class is the source of certain monitor methods

• This means that using the Java syntax of synchronization, a programmer could write a class that embodied all of the characteristics of a monitor

Page 222: Chapter 6, Process Synchronization

222

• Note also that the discussion will shortly mention the idea of a Condition variable.

• Java has a Condition interface which corresponds to this idea

• Once again, a programmer could write code that agreed with the discussion of monitors here

Page 223: Chapter 6, Process Synchronization

223

• Finally, note that Java synchronization will be the next topic and some of the syntax will be covered.

• When it is covered, the intent is not to show in particular how to implement a monitor—but how to use it directly to synchronize programmer written code.

• Java synchronization may be built on the monitor concept, but that doesn’t mean that the programmer has to do synchronization using monitors directly

Page 224: Chapter 6, Process Synchronization

224

Condition Variables (or Objects)

• A monitor class can have declared in it Condition variables:

• private Condition x, y;• A monitor class will also have two special

methods:• wait() and signal()

Page 225: Chapter 6, Process Synchronization

225

• In the Object class of Java there is a wait() method which is like the conceptual wait() method in a monitor

• In the Object class of Java there are also methods notify() and notifyAll().

• These methods correspond to the conceptual signal() method in a monitor

Page 226: Chapter 6, Process Synchronization

226

• Different threads may share a reference to a monitor object

• The threads can call methods on the monitor• A monitor method may contain a call of this

form:• x.wait();• If this happens, the thread that was “running

in the monitor” is suspended

Page 227: Chapter 6, Process Synchronization

227

• A thread will remain suspended until another thread makes a call such as this:

• x.signal()• In Java this would be x.notify() or x.notifyAll()

Page 228: Chapter 6, Process Synchronization

228

• In a primitive semaphore, if a resource is not available, when a process calls acquire() and fails, the process goes into a spinlock

• The logic of wait() is sort of the reverse: • A process can voluntarily step aside by calling

x.wait(), allowing another thread into the protected code

Page 229: Chapter 6, Process Synchronization

229

• It becomes second nature to think of concurrency control as a technique for enforcing mutual exclusion on a resource

• Recall that synchronization also includes the ability to enforce a particular execution order

Page 230: Chapter 6, Process Synchronization

230

• It may be easier to remember the idea underlying wait() by thinking of it as a tool that makes it possible for a process to take actions which affect its execution order

• Although not directly related, it may be helpful to remember the concept of “politeness” that came up in the Alphonse and Gaston phase of trying to explain concurrency control

• Making a wait() call allows other threads to go first

Page 231: Chapter 6, Process Synchronization

231

• More points to consider:• The authors raise the question of what it would

mean to call “signal()” (I gather they mean release()) on a semaphore when there is nothing to release

• If you went back to look at the original pseudo-code for a semaphore, you would find that this would increment the counter—even though that would put the count above the number of actual resources

Page 232: Chapter 6, Process Synchronization

232

• It may or may not be possible to fix the pseudo-code to deal with this—but it’s not important,

• Because if you looked at the original pseudo-code, reference is made to a list of processes waiting for a resource—and the implementation of this wasn’t explained

Page 233: Chapter 6, Process Synchronization

233

• The previous overhead was a jumping off point for this important, additional information about monitors:

• The monitor concept explicitly includes waiting lists

• If a thread running in the monitor causes a call such as x.wait() to be made, the thread is put in the waiting list

Page 234: Chapter 6, Process Synchronization

234

• When another thread makes a call x.notify(), that thread will step aside and one in the waiting list will be resumed

• If another thread were to make a call x.notifyAll(), all waiting threads would potentially be resumed

Page 235: Chapter 6, Process Synchronization

235

• The management of waiting lists and the ability to call wait(), notify(), and notifyAll() leads to another consideration which the implementation of a monitor has to take into account

• Let this scenario be given:• Thread Q is waiting because it earlier called x.wait()• Thread P is running and it calls x.signal()• By definition, only one of P and Q can be running in

the monitor at the same time

Page 236: Chapter 6, Process Synchronization

236

• The question becomes, what protocol should be used to allow Q to begin running in the monitor instead of P?

• This question is not one that has to be answered by the application programmer

• It is a question that confronts the designer of a particular monitor implementation

Page 237: Chapter 6, Process Synchronization

237

• In general, there are two alternatives:• Signal and wait: • P signals, then waits, allowing Q its turn. • After Q finishes, P resumes.

Page 238: Chapter 6, Process Synchronization

238

• Signal and continue: • P signals and continues until it leaves the

monitor. • At that point Q can enter the monitor (or

potentially may not, if prevented by some other condition)

Page 239: Chapter 6, Process Synchronization

239

• The book next tries to illustrate the use of monitors in order to solve the dining philosophers problem

• I am not going to cover this

Page 240: Chapter 6, Process Synchronization

240

6.8 Java Synchronization

• Thread safe. Definition: Concurrent threads have been implemented so that they leave shared data in a consistent state

• Note: Much of the example code shown previously would not be thread safe. It is highly likely that threaded code without the use of synchronization syntax would generate a compiler error or warning indicating that it was not thread safe

Page 241: Chapter 6, Process Synchronization

241

• The most recent examples which used a semaphore class which did use the Java synchronization syntax internally should not generate this error/warning

• If code does produce this in warning form and the code can be run, it should be made emphatically clear that even though it runs, it IS NOT THREAD SAFE

Page 242: Chapter 6, Process Synchronization

242

• In other words, unsafe code may appear to run

• More accurately it may run and even give correct results some or most of the time

• But depending on the vagaries of thread scheduling, at completely unpredictable times, it will give incorrect results

Page 243: Chapter 6, Process Synchronization

243

• If you compiled the code for the Peterson’s solution code given earlier, this defect would hold.

• That example did not actually include any functioning synchronization mechanism on the shared variables that modeled turn and desire

Page 244: Chapter 6, Process Synchronization

244

• More repetitive preliminaries:– The idea of inconsistent state can be illustrated

with the producer-consumer problem: – If not properly synchronized, calls to insert() and

remove() can result in an incorrect count of how many messages are in a shared buffer

Page 245: Chapter 6, Process Synchronization

245

– Keep in mind also that the Java API supports synchronization syntax at the programmer level

– However, all synchronization ultimately is provided by something like a test and set instruction at the hardware level

Page 246: Chapter 6, Process Synchronization

246

• Because this is such a long and twisted path, the book reviews more of the preliminaries

• To begin with, the initial examples are not even truly synchronized.

• This means that they are incorrect. • They will lead to race conditions on shared

variables/shared objects

Page 247: Chapter 6, Process Synchronization

247

• Although not literally correct, the initial examples attempt to illustrate what is behind synchronization by introducing the concept of busy waiting or a spin lock

• The basic idea is that if one thread holds a resource, another thread wanting that resource will have to wait in some fashion

• In the illustrative, programmer-written code, this waiting takes the form of sitting in a loop

Page 248: Chapter 6, Process Synchronization

248

• The first problem with busy waiting is that it’s wasteful

• A thread that doesn’t have a resource it needs can be scheduled and burn up CPU cycles spinning in a loop until its time slice expires

• The second problem with busy waiting is that it can lead to livelock

Page 249: Chapter 6, Process Synchronization

249

• Livelock is not quite the same as deadlock• In deadlock, two threads “can’t move”

because each is waiting for an action that only the other can take

• In livelock, both threads are alive and scheduled, but they still don’t make any progress

Page 250: Chapter 6, Process Synchronization

250

• The book suggests this illustrative scenario:• A producer has higher priority than a consumer• The producer fills the shared buffer and remains

alive, keeping on trying to add more messages• The consumer, having lower priority, is alive, but

never scheduled, so it can never remove a message from the buffer

• Thus, the producer can never enter a new message into it

Page 251: Chapter 6, Process Synchronization

251

• Using real syntax that correctly enforces mutual exclusion can lead to deadlock

• Deadlock is a real problem in the development of synchronized code, but it is not literally a problem of synchronization syntax

• In other words, you can write an example that synchronizes correctly but still has this problem

Page 252: Chapter 6, Process Synchronization

252

• A simplistic example would be an implementation of the dining philosophers where each one could pick up the left chopstick

• The problem is not that there is uncontrolled access to a shared resource. The problem is that once that state has been entered, it will never be left

• Java synchronization syntax can be introduced and illustrated and the question of how to prevent or resolve deadlocks can be put off until chapter 7, which is devoted to that question

Page 253: Chapter 6, Process Synchronization

253

• The book takes the introduction of synchronization syntax through two stages:

• Stage 1: You use Java synchronization and the Thread class yield() method to write code that does enforce mutual exclusion and which is essentially a “correct” implementation of busy waiting.

• This is wasteful and livelock prone, but it is synchronized

Page 254: Chapter 6, Process Synchronization

254

• Stage 2: You use Java synchronization with the wait(), notify(), and notifyAll() methods of the Object class.

• Instead of busy waiting, this relies on the underlying monitor-like capabilities of Java to have threads wait in queues or lists.

• This is deadlock prone, but it deals with all of the other foregoing problems

Page 255: Chapter 6, Process Synchronization

255

The synchronized Keyword in Java

• Java synchronization is based on the monitor concept, and this descends all the way from the Object class

• Every object in Java has a lock associated with it

• This lock is essentially like a simple monitor• Locking for the object is based on a single

condition variable

Page 256: Chapter 6, Process Synchronization

256

• If you are not writing synchronized code—if you are not using the keyword synchronized, the object’s lock is completely immaterial

• It is a system supplied feature of the object which lurks in the background unused by you and having no effect on what you are doing

Page 257: Chapter 6, Process Synchronization

257

• Inside the code of a class, methods can be declared synchronized

• In the monitor concept, mutual exclusion is enforced on all of the methods of a class at the same time

• Java is finer-grained. • There can be unsynchronized methods• However, if >1 method is declared synchronized in a

class, then mutual exclusion is enforced across all of them at the same time for any threads trying to access the object

Page 258: Chapter 6, Process Synchronization

258

• If a method is synchronized and no thread holds the lock, the first thread that calls the method acquires the lock

• Again, Java synchronization is monitor-like. • There is an entry set for the lock, in other words,

a waiting list• If another thread calls a synchronized method

and cannot acquire the lock, it is put into the entry set for that lock

Page 259: Chapter 6, Process Synchronization

259

• When the thread holding the lock finishes running whatever synchronized method it was in, it releases the lock

• At that point, if the entry set has threads in it, the JVM will schedule one

• FIFO scheduling may be done on the entry set, but the Java specifications don’t require it

Page 260: Chapter 6, Process Synchronization

260

• Here are the first correctly synchronized snippets of sample code which the book offers

• They do mutual exclusion on a shared buffer• However, they mimic busy waiting using the

Thread class yield() method• The Java API simply says this about the method:• “Causes the currently executing thread object to

temporarily pause and allow other threads to execute.”

Page 261: Chapter 6, Process Synchronization

261

• The book doesn’t bother to give a complete set of classes for this solution because it is not a very good one

• Because it implements a kind of busy waiting, it’s wasteful and livelock prone.

• The book also claims that it is prone to deadlock.• It seems that even the book may be getting hazy about

the exact defects of given solutions. • Let it be said that this solution, at the very least, is X-

lock prone

Page 262: Chapter 6, Process Synchronization

262

Synchronized insert() and remove() Methods for Producers and Consumers of a Bounded Buffer

• public synchronized void insert(Object item)

• {• while(count == BUFFER_SIZE)• Thread.yield;• ++count;• buffer[in] = item;• in = (in + 1) % BUFFER_SIZE;• }

Page 263: Chapter 6, Process Synchronization

263

• public synchronized Object remove()• {• Object item;• while(count == 0)• Thread.yield();• --count;• item = buffer[out];• out = (out + 1) % BUFFER_SIZE;• return item;• }

Page 264: Chapter 6, Process Synchronization

264

• Note that in the fully semaphore oriented (pseudo?) solution, there were three semaphores

• One handled the mutual exclusion which the keyword synchronized handles here

• The other two handled the cases where the buffer was empty or full

Page 265: Chapter 6, Process Synchronization

265

• There is no such thing as a synchronized “empty” or “full” variable, so there are not two additional uses of synchronized in this code

• The handling of the empty and full cases goes all the way back to the original example, where the code does modular arithmetic and keeps a count variable

• Note that the call to yield() then depends on the value of the count variable

Page 266: Chapter 6, Process Synchronization

266

Code with synchronized and wait(), notify(), and notifyAll()

• Java threads can call methods wait(), notify(), and notifyAll()

• These methods are similar in function to the methods of these names described when discussing the monitor concept

• Each Java object has exactly one lock, but it has two sets, the entry set and the wait set

Page 267: Chapter 6, Process Synchronization

267

The Entry Set

• The entry set is a waiting list• You can think of it as being implemented as a

linked data structure containing the “PCB’s” of threads

• Threads in the entry set are those which have reached the point in execution where they have called a synchronized method but can’t get in because another thread holds the lock

Page 268: Chapter 6, Process Synchronization

268

• A thread leaves the entry set and enters the synchronized method it wishes to run when the current lock holder releases the lock and the scheduling algorithm picks from the entry set that thread wanting the lock

Page 269: Chapter 6, Process Synchronization

269

• The wait set is also a waiting list• You can also think of this as a linked data

structure containing the “PCB’s” of threads• The wait set is not the same as the entry set• Suppose a thread holds a lock on an object• A thread enters the wait set by calling the

wait() method

Page 270: Chapter 6, Process Synchronization

270

• Entering the wait set means that the thread voluntarily releases the lock that it holds

• In the application code this would be triggered in an if statement where some (non-lock related) condition has been checked and it has been determined that due to that condition the thread can’t continue executing anyway

• When a thread is in the wait set, it is blocked. It can’t be scheduled but it’s not burning up resources because it’s not busy waiting

Page 271: Chapter 6, Process Synchronization

271

The Entry and Wait Sets Can Be Visualized in this Way

Page 272: Chapter 6, Process Synchronization

272

• By definition, threads in the wait set are not finished with the synchronized code

• Threads acquire the synchronized code through the entry set

• There has to be a mechanism for a thread in the wait set to get into the entry set

Page 273: Chapter 6, Process Synchronization

273

The Way to Move a Thread from the Wait Set to the Entry Set

• If in the synchronized code, one or more calls to wait() have been made,

• At the end of the code for a synchronized method, put a call to notify()

• When the system handles the notify() call, it picks an arbitrary thread from the wait set and puts it into the entry set

• When the thread is moved from one set to the other, its state is changed from blocked to runnable

Page 274: Chapter 6, Process Synchronization

274

• The foregoing description should be sufficient for code that manages two threads

• As a consequence, it should provide enough tools for an implementation of the producer-consumer problem using Java synchronization

Page 275: Chapter 6, Process Synchronization

275

Preview of the Complete Producer-Consumer Code

• The BoundedBuffer class has two methods, insert() and remove()

• These two methods are synchronized• Synchronization of the methods protects both

the count variable and the buffer itself, since each of these things is only accessed and manipulated through these two methods

Page 276: Chapter 6, Process Synchronization

276

• Unlike with semaphores, the implementation is nicely parallel:

• You start both methods with a loop containing a call to wait() and end both with a call to notify()

• Note that it is not immediately clear why the call to wait() is in loop rather than an if statement

• Note also, syntactically, that the call to wait() has to occur in a try block

Page 277: Chapter 6, Process Synchronization

277

• Finally, note these important points:• The use of the keyword synchronized enforces

mutual exclusion• The use of wait() and notify() have taken over the

job of controlling whether a thread can insert or remove a message from the buffer depending on whether the buffer is full or not

• The code follows. • This will be followed by further commentary

Page 278: Chapter 6, Process Synchronization

278

• /**• * BoundedBuffer.java• * • * This program implements the bounded buffer using Java synchronization.• * • */

• public class BoundedBuffer implements Buffer {• private static final int BUFFER_SIZE = 5;

• private int count; // number of items in the buffer

• private int in; // points to the next free position in the buffer

• private int out; // points to the next full position in the buffer

• private Object[] buffer;

• public BoundedBuffer() {• // buffer is initially empty• count = 0;• in = 0;• out = 0;

• buffer = new Object[BUFFER_SIZE];• }

Page 279: Chapter 6, Process Synchronization

279

• public synchronized void insert(Object item) {• while (count == BUFFER_SIZE) {• try {• wait();• } catch (InterruptedException e) {• }• }

• // add an item to the buffer• ++count;• buffer[in] = item;• in = (in + 1) % BUFFER_SIZE;

• if (count == BUFFER_SIZE)• System.out.println("Producer Entered " + item + " Buffer FULL");• else• System.out.println("Producer Entered " + item + " Buffer Size =

"• + count);

• notify();• }

Page 280: Chapter 6, Process Synchronization

280

• // consumer calls this method• public synchronized Object remove() {• Object item;

• while (count == 0) {• try {• wait();• } catch (InterruptedException e) {• }• }

• // remove an item from the buffer• --count;• item = buffer[out];• out = (out + 1) % BUFFER_SIZE;

• if (count == 0)• System.out.println("Consumer Consumed " + item + " Buffer EMPTY");• else• System.out.println("Consumer Consumed " + item + " Buffer Size = "• + count);

• notify();

• return item;• }

• }

Page 281: Chapter 6, Process Synchronization

281

An example scenario showing how the calls to wait() and notify() work

• Assume that the lock is available but the buffer is full

• The producer calls insert()• The lock is available so it gets in• The buffer is full so it calls wait()• The producer releases the lock, gets blocked,

and is put in the wait set

Page 282: Chapter 6, Process Synchronization

282

• The consumer eventually calls remove()• There is no problem because the lock is

available• At the end of removing, the consumer calls

notify()• The call to notify() removes the producer from

the wait set, puts it into the entry set, and makes it runnable

Page 283: Chapter 6, Process Synchronization

283

• When the consumer exits the remove() method, it gives up the lock

• The producer can now be scheduled• The producer thread begins execution at the line of

code following the wait() call which caused it to be put into the wait set

• After inserting, the producer calls notify()• This would allow any other waiting thread to run• If nothing was waiting, it has no effect

Page 284: Chapter 6, Process Synchronization

284

• The rest of the code is given here so it’s close by for reference

• It is the same as the rest of the code for the previous examples, so it may not be necessary to look at it again

Page 285: Chapter 6, Process Synchronization

285

• /**• * An interface for buffers• *• */

• public interface Buffer• {• /**• * insert an item into the Buffer.• * Note this may be either a blocking• * or non-blocking operation.• */• public abstract void insert(Object item);

• /**• * remove an item from the Buffer.• * Note this may be either a blocking• * or non-blocking operation.• */• public abstract Object remove();• }

Page 286: Chapter 6, Process Synchronization

286

• /**• * This is the producer thread for the bounded buffer problem.• */

• import java.util.*;

• public class Producer implements Runnable {• private Buffer buffer;

• public Producer(Buffer b) {• buffer = b;• }

• public void run() {• Date message;

• while (true) {• System.out.println("Producer napping");• SleepUtilities.nap();

• // produce an item & enter it into the buffer• message = new Date();• System.out.println("Producer produced " + message);

• buffer.insert(message);• }• }

• }

Page 287: Chapter 6, Process Synchronization

287

• /**• * This is the consumer thread for the bounded buffer problem.• */• import java.util.*;

• public class Consumer implements Runnable {• private Buffer buffer;

• public Consumer(Buffer b) {• buffer = b;• }

• public void run() {• Date message;

• while (true) {• System.out.println("Consumer napping");• SleepUtilities.nap();

• // consume an item from the buffer• System.out.println("Consumer wants to consume.");

• message = (Date) buffer.remove();• }• }

• }

Page 288: Chapter 6, Process Synchronization

288

• /**• * This creates the buffer and the producer and consumer threads.• *• */• public class Factory• {• public static void main(String args[]) {• Buffer server = new BoundedBuffer();

• // now create the producer and consumer threads• Thread producerThread = new Thread(new Producer(server));• Thread consumerThread = new Thread(new Consumer(server));• • producerThread.start();• consumerThread.start(); • }• }

Page 289: Chapter 6, Process Synchronization

289

• /**• * Utilities for causing a thread to sleep.• * Note, we should be handling interrupted exceptions• * but choose not to do so for code clarity.• */

• public class SleepUtilities• {• /**• * Nap between zero and NAP_TIME seconds.• */• public static void nap() {• nap(NAP_TIME);• }

• /**• * Nap between zero and duration seconds.• */• public static void nap(int duration) {• int sleeptime = (int) (duration * Math.random() );• try { Thread.sleep(sleeptime*1000); }• catch (InterruptedException e) {}• }

• private static final int NAP_TIME = 5;• }

Page 290: Chapter 6, Process Synchronization

290

Multiple Notifications

• A call to notify() picks one thread out of the wait set and puts it into the entry set

• What if there are >1 waiting threads?• The book points out that using notify() alone

can lead to deadlock• This is an important problem, which motivates

a discussion of notifyAll(), but the topic will not be covered in detail until the next chapter

Page 291: Chapter 6, Process Synchronization

291

• The general solution to any problems latent in calling notify() is to call notifyAll()

• This moves all of the waiting threads to the entry set

• At that point, which one runs next depends on the scheduler

• The selected one may immediately block

Page 292: Chapter 6, Process Synchronization

292

• However, if notifyAll() is always called, statistically, if there is at least one thread that can run, it will eventually be scheduled

• Any threads which depend on it could then run when they are scheduled, and progress will be made

Page 293: Chapter 6, Process Synchronization

293

notifyAll() and the Readers-Writers Problem

• The book gives full code for this• I will try to abstract their illustration without

referring to the complete code• Remember that a read lock is not exclusive– Multiple reading threads are OK at the same time– Only writers have to be blocked

• Write locks are exclusive– Any one writer blocks all other readers and writers

Page 294: Chapter 6, Process Synchronization

294

Synopsis of Read Lock Code• acquireReadLock()• {• while(…)• wait();• …• }• releaseReadLock()• {• …• notify();

Page 295: Chapter 6, Process Synchronization

295

• One writer will be notified when the readers finish.

• It does seem possible to call notifyAll(), in which case possibly >1 writer would contend to be scheduled, but it is sufficient to just ask the system to notify one waiting thread.

Page 296: Chapter 6, Process Synchronization

296

Synopsis of Write Lock Code• acquireWriteLock()• {• while(…)• wait();• …• }• releaseReadLock()• {• …• notifyAll();• }

Page 297: Chapter 6, Process Synchronization

297

• All readers will be notified when the writer finishes (as well as any waiting writers).

• The point is to get all of the readers active since they are all allowed to read concurrently

Page 298: Chapter 6, Process Synchronization

298

Block Synchronization

• Lock scope definition: Time between when a lock is acquired and released (might also refer to the location in the code where it’s in effect)

• Declaring a method synchronized may lead to an unnecessarily long scope if large parts of the method don’t access the shared resource

• Java supports block synchronization syntax where just part of a method is made into a critical section

Page 299: Chapter 6, Process Synchronization

299

• Block synchronization is based on the idea that every object has a lock

• You can construct an instance of the Object class and use it as the lock for a block of code

• In other words, you use the lock of that object as the lock for the block

• Example code follows

Page 300: Chapter 6, Process Synchronization

300

• Object mutexLock = new Object();• …• public void someMethod()• {• nonCriticalSection();• synchronized(mutexLock)• {• criticalSection();• }• remainderSection();• }

Page 301: Chapter 6, Process Synchronization

301

• Block synchronization also allows the use of wait() and notify calls()• Object mutexLock = new Object();• …• synchronized(mutexLock)• {• …• try• {• mutexLock.wait();• catch(InterruptedException ie)• {• …• }• …• Synchronized(mutexLock)• {• mutexLock.notify();• }

Page 302: Chapter 6, Process Synchronization

302

Synchronization Rules: I.e., Rules Affecting the Use of the Keyword synchronized

• 1. A thread that owns the lock for an object can enter another synchronized method (or block) for the same object.

• This is known as a reentrant or recursive lock.• 2. A thread can nest synchronized calls for

different objects. • One thread can hold the lock for >1 object at

the same time.

Page 303: Chapter 6, Process Synchronization

303

• 3. Some methods of a class may not be declared synchronized.

• A method that is not declared synchronized can be called regardless of lock ownership—that is, whether a thread is running in a synchronized method concurrently

• 4. If the wait set for an object is empty, a call to notify() or notifyAll() has no effect.

Page 304: Chapter 6, Process Synchronization

304

• 5. wait(), notify(), and notifyAll() can only be called from within synchronized methods or blocks.

• Otherwise, an IllegalMonitorStateException is thrown.

• An additional note: For every class, in addition to the lock that every object of that class gets, there is also a class lock.

• That makes it possible to declare static methods or blocks in static methods synchronized

Page 305: Chapter 6, Process Synchronization

305

Handling the InterruptedException

• If you go back to chapter 4, you’ll recall that the topic of asynchronous (immediate) and deferred thread cancellation (termination) came up

• Deferred cancellation was preferred. • This meant that threads were cancelled by

calling interrupt() rather than stop()

Page 306: Chapter 6, Process Synchronization

306

• Under this scenario, thread1 might have a reference to thread2

• Within the code for thread1, thread2 would be interrupted in this way:

• thread2.interrupt();

Page 307: Chapter 6, Process Synchronization

307

• Then thread2 can check its status with one of these two calls:

• me.interrupted();• me.isInterrupted();• thread2 can then do any needed

housekeeping (preventing inconsistent state) before terminating itself

Page 308: Chapter 6, Process Synchronization

308

• The question becomes, is it possible to interrupt (cancel) a thread that is a wait set (is blocked)?

• A call to wait() has to occur in a block like this:• try• {• wait();• }• catch(InterruptedException ie)• {• …• }

Page 309: Chapter 6, Process Synchronization

309

• If a thread calls wait(), it goes into the wait set and stops executing

• As explained up to this point, the thread can’t resume until notify() or notifyAll() are called and it is picked for scheduling

• This isn’t entirely true

Page 310: Chapter 6, Process Synchronization

310

• The wait() call is the last live call of the thread• The system is set up so that thread1 might

make a call like this while thread2 is in the wait set:

• thread2.interrupt();

Page 311: Chapter 6, Process Synchronization

311

• If such a call is made on thread2 while it’s in the wait set, the system will throw an exception back out where thread2 made the call to wait()

• At that point, thread2 is no longer blocked because it’s kicked out of the wait set

• This means that thread2 can be scheduled without a call to notify(), but its state is now interrupted

• If you choose to handle the exception, then what you should do is provide the housekeeping code which thread2 needs to run so that it can terminate itself and leave a consistent state

Page 312: Chapter 6, Process Synchronization

312

• The foregoing can be summarized as follows:• Java has this mechanism so that threads can be terminated

even after they’ve disappeared into a wait set• This can be useful because there should be no need for a

thread to either waste time in the wait set or run any further if it is slated for termination anyway

• This is especially useful because it allows a thread which is slated for termination to release any locks or resources it might be holding.

• Why this is good will become even clearer in the following chapter, on deadlocks

Page 313: Chapter 6, Process Synchronization

313

Concurrency Features in Java

• What follows is just a listing of the features with minimal explanation

• If you want to write synchronized code in Java, check the API documentation

• 1. There is a class named ReentrantLock. • This supports functionality similar to the

synchronized keyword (or a semaphore) with added features like enforcing fairness in scheduling threads waiting for locks

Page 314: Chapter 6, Process Synchronization

314

• 2. There is a class named Semaphore. • Technically, the examples earlier were based on the authors’

hand-coded semaphore. • If you want to use the Java Semaphore class, double check

its behavior in the API• 3. There is an interface named Condition, and this type can

be used to declare condition variables associated with reentrant locks.

• They are related to the idea of condition variables in a monitor, and they are used with wait(), notify(), and notifyAll() with reentrant locks

Page 315: Chapter 6, Process Synchronization

315

• 6.9 Synchronization Examples: Solaris, XP, Linux, Pthreads. SKIP

• 6.10 Atomic Transactions: This is a fascinating topic that has as much to do with databases as operating systems… SKIP

• 6.11 Summary. SKIP

Page 316: Chapter 6, Process Synchronization

316

The End