Sei sulla pagina 1di 53

Lecture 5

Synchronization

Operating System Concepts 9th Edition

Silberschatz, Galvin and Gagne 2013

Process Synchronization

Background
The Critical-Section Problem
Petersons Solution
Synchronization Hardware
Mutex Locks
Semaphores
Classic Problems of Synchronization
Monitors
Synchronization Examples
Alternative Approaches

Objectives

To introduce the critical-section problem, whose solutions can be used to ensure the consistency of shared
data

To present both software and hardware solutions of the critical-section problem

To examine several classical process-synchronization problems

To explore several tools that are used to solve process synchronization problems

Background

Processes can execute concurrently

May be interrupted at any time, partially completing execution

Concurrent access to shared data may result in data inconsistency

Maintaining data consistency requires mechanisms to ensure the orderly execution of cooperating
processes

Illustration of the problem:


Suppose that we wanted to provide a solution to the consumer-producer problem that fills all the buffers.
We can do so by having an integer counter that keeps track of the number of full buffers. Initially,
counteris set to 0. It is incremented by the producer after it produces a new buffer and is decremented
by the consumer after it consumes a buffer.

Producer
while (true) {
/* produce an item in next produced */
while (counter == BUFFER_SIZE) ;
/* do nothing */
buffer[in] = next_produced;
in = (in + 1) % BUFFER_SIZE;
counter++;
}

Consumer
while (true) {
while (counter == 0)
; /* do nothing */
next_consumed = buffer[out];
out = (out + 1) % BUFFER_SIZE;

counter--;

/* consume the item in next consumed */


}

Race Condition

counter++ could be implemented as


register1 = counter
register1 = register1 + 1
counter = register1

counter-- could be implemented as


register2 = counter
register2 = register2 - 1
counter = register2

Consider this execution interleaving with count = 5 initially:


S0: producer execute register1 = counter
S1: producer execute register1 = register1 + 1
S2: consumer execute register2 = counter
S3: consumer execute register2 = register2 1
S4: producer execute counter = register1
S5: consumer execute counter = register2

{register1 = 5}
{register1 = 6}
{register2 = 5}
{register2 = 4}
{counter = 6 }
{counter = 4}

Critical Section Problem

Consider system of n processes {p0, p1, pn-1}

Each process has critical section segment of code

Process may be changing common variables, updating table, writing file, etc

When one process in critical section, no other may be in its critical section

Critical section problem is to design protocol to solve this

Each process must ask permission to enter critical section in entry section, may follow critical section with exit
section, then remainder section

Critical Section

General structure of process pi is

Solution to Critical-Section Problem


1. Mutual Exclusion - If process Pi is executing in its critical section, then no other processes can be executing
in their critical sections
2. Progress - If no process is executing in its critical section and there exist some processes that wish to enter
their critical section, then the selection of the processes that will enter the critical section next cannot be
postponed indefinitely
3. Bounded Waiting - A bound must exist on the number of times that other processes are allowed to enter
their critical sections after a process has made a request to enter its critical section and before that request
is granted

Assume that each process executes at a nonzero speed


No assumption concerning relative speed of the n processes

Two approaches depending on if kernel is preemptive or non-preemptive


Preemptive allows preemption of process when running in kernel mode
Non-preemptive runs until exits kernel mode, blocks, or voluntarily yields CPU
Essentially free of race conditions in kernel mode

Petersons Solution

Good algorithmic description of solving the problem

Two process solution

Assume that the load and store instructions are atomic; that is, cannot be interrupted

The two processes share two variables:


int turn;
Boolean flag[2]

The variable turn indicates whose turn it is to enter the critical section

The flag array is used to indicate if a process is ready to enter the critical section. flag[i] =
true implies that process Pi is ready!

Algorithm for Process Pi


do {
flag[i] = true;
turn = j;
while (flag[j] && turn == j);
critical section
flag[i] = false;
remainder section
} while (true);

Provable that

1.

Mutual exclusion is preserved

2.

Progress requirement is satisfied

3.

Bounded-waiting requirement is met

Synchronization Hardware

Many systems provide hardware support for critical section code

All solutions below based on idea of locking


Protecting critical regions via locks

Uniprocessors could disable interrupts


Currently running code would execute without preemption
Generally too inefficient on multiprocessor systems
Operating systems using this not broadly scalable

Modern machines provide special atomic hardware instructions


Atomic = non-interruptible
Either test memory word and set value
Or swap contents of two memory words

Solution to Critical-section Problem Using Locks


do {
acquire lock
critical section
release lock
remainder section
} while (TRUE);

test_and_set Instruction

Definition:

boolean test_and_set (boolean *target)


{
boolean rv = *target;
*target = TRUE;
return rv:
}

Solution using test_and_set()

Shared boolean variable lock, initialized to FALSE


Solution:

do {
while (test_and_set(&lock))
; /* do nothing */
/* critical section */
lock = false;
/* remainder section */
} while (true);

compare_and_swap Instruction

Definition:

int compare_and_swap(int *value, int expected, int new_value) {


int temp = *value;
if (*value == expected)
*value = new_value;
return temp;
}

Solution using compare_and_swap

Shared Boolean variable lock initialized to FALSE; Each process has a local Boolean variable key
Solution:

do {
while (compare_and_swap(&lock, 0, 1) != 0)
; /* do nothing */
/* critical section */
lock = 0;
/* remainder section */
} while (true);

Bounded-waiting Mutual Exclusion with test_and_set


do {
waiting[i] = true;
key = true;
while (waiting[i] && key)
key = test_and_set(&lock);
waiting[i] = false;
/* critical section */
j = (i + 1) % n;
while ((j != i) && !waiting[j])
j = (j + 1) % n;
if (j == i)
lock = false;
else
waiting[j] = false;
/* remainder section */
} while (true);

Mutex Locks
Previous solutions are complicated and generally inaccessible to application
programmers
OS designers build software tools to solve critical section problem
Simplest is mutex lock
Product critical regions with it by first acquire() a lock then release() it
Boolean variable indicating if lock is available or not

Calls to acquire() and release() must be atomic


Usually implemented via hardware atomic instructions

But this solution requires busy waiting


This lock therefore called a spinlock

acquire() and release()


acquire() {
while (!available)
; /* busy wait */
available = false;;
}
release() {
available = true;
}
do {
acquire lock
critical section
release lock
remainder section
} while (true);

Synchronization W.R.T. to
Threads

Operating System Concepts 9th Edition

Silberschatz, Galvin and Gagne 2013

Temporal relations

Instructions executed by a single thread are totally ordered

A<B<C<

Absent synchronization, instructions executed by distinct threads must be considered unordered / simultaneous

Not X < X, and not X < X

Example: Example
In the beginning...
main()

Y-axis is time.

A
pthread_create()

foo()

A'

B'
C

Could be one CPU, could


be multiple CPUs (cores).

A<B<C
A' < B'
A < A'
C == A'
C == B'

Critical Sections / Mutual Exclusion

Sequences of instructions that may get incorrect results if executed simultaneously are called critical sections
(We also use the term race condition to refer to a situation in which the results depend on timing)
Mutual exclusion means not simultaneous
A < B or B < A
We dont care which
Forcing mutual exclusion between two critical section executions is sufficient to ensure correct execution
guarantees ordering
One way to guarantee mutually exclusive execution is using locks

Critical sections
is the happens-before relation

T1

T2

Possibly incorrect

T1

T2

Correct

T1

T2

Correct

When do critical sections arise?

One common pattern:

read-modify-write of

a shared value (variable)

in code that can be executed concurrently


(Note: There may be only one copy of the code (e.g., a procedure), but it can be executed by more than one
thread at a time)

Shared variable:

Globals and heap-allocated variables

NOT local variables (which are on the stack)


(Note: Never give a reference to a stack-allocated (local) variable to another thread, unless youre
superhumanly careful )

Example: buffer management

Threads cooperate in multithreaded programs

to share resources, access shared data structures

e.g., threads accessing a memory cache in a web server

also, to coordinate their execution

e.g., a disk reader thread hands off blocks to a network writer thread through a circular buffer

disk reader
thread

network
writer
thread

circular
buffer

Example: shared bank account

Suppose we have to implement a function to withdraw money from a bank account:

int withdraw(account, amount) {


int balance = get_balance(account);
balance -= amount;

// read

// modify

put_balance(account, balance);

// write

spit out cash;


}

Now suppose that you and your partner share a bank account with a balance of $100.00

what happens if you both go to separate ATM machines, and simultaneously withdraw $10.00 from the
account?

Assume the banks application is multi-threaded

A random thread is assigned a transaction when that transaction is submitted

int withdraw(account, amount) {

int withdraw(account, amount) {

int balance = get_balance(account);

int balance = get_balance(account);

balance -= amount;

balance -= amount;

put_balance(account, balance);

put_balance(account, balance);

spit out cash;

spit out cash;


}

Interleaved schedules

The problem is that


the execution of the
two threads can be
interleaved, assuming
preemptive
scheduling:

balance = get_balance(account);
balance -= amount;

Execution sequence
as seen by CPU

balance = get_balance(account);

context switch

balance -= amount;
put_balance(account, balance);
spit out cash;

Whats the account


balance after this
sequence?

whos happy, the


bank or you?

How often is this


sequence likely to
occur?

put_balance(account, balance);
spit out cash;

context switch

Other Execution Orders

Which interleavings are ok? Which are not?

int withdraw(account, amount) {

int withdraw(account, amount) {

int balance = get_balance(account);

int balance = get_balance(account);

balance -= amount;

balance -= amount;

put_balance(account, balance);

put_balance(account, balance);

spit out cash;

spit out cash;


}

How About Now?


int xfer(from, to, amt) {
withdraw( from, amt );

withdraw( from, amt );

deposit( to, amt );

deposit( to, amt );

int xfer(from, to, amt) {

Morals:

Interleavings are hard to reason about

We make lots of mistakes

Control-flow analysis is hard for tools to get right

Identifying critical sections and ensuring mutually exclusive access is easier

Another example
i++;

i++;

Correct critical section requirements

Correct critical sections have the following requirements

mutual exclusion

progress

at most one thread is in the critical section


if thread T is outside the critical section, then T cannot prevent thread S from entering the critical
section

bounded waiting (no starvation)

if thread T is waiting on the critical section, then T will eventually enter the critical section

assumes threads eventually leave critical sections

performance

the overhead of entering and exiting the critical section is small with respect to the work being
done within it

Mechanisms for building critical sections

Spinlocks

Semaphores (and non-spinning locks)

primitive, minimal semantics; used to build others


basic, easy to get the hang of, somewhat hard to program with

Monitors

higher level, requires language support, implicit operations

easier to program with; Java synchronized() as an example

Messages

simple model of communication and synchronization based on (atomic) transfer of data across a channel

direct application to distributed systems

Locks

A lock is a memory object with two operations:

acquire(): obtain the right to enter the critical section

release(): give up the right to be in the critical section

acquire() prevents progress of the thread until the lock can be acquired

(Note: terminology varies: acquire/release, lock/unlock)

Locks:
Example
Locks:
Example
execution

lock()
lock()
unlock()

unlock()

Two choices:
Spin
Block
(Spin-then-block)

Acquire/Release

Threads pair up calls to acquire() and release()

between acquire()and release(), the thread holds the lock

acquire() does not return until the caller owns (holds) the lock

at most one thread can hold a lock at a time

What happens if the calls arent paired (I acquire, but neglect to release)?

What happens if the two threads acquire different locks (I think that access to a particular shared data
structure is mediated by lock A, and you think its mediated by lock B)?

(granularity of locking)

Using locks
acquire(lock)
balance = get_balance(account);

int withdraw(account, amount) {

balance -= amount;

balance = get_balance(account);
balance -= amount;
put_balance(account, balance);

critical
section

acquire(lock);

acquire(lock)
put_balance(account, balance);
release(lock);

release(lock);

balance = get_balance(account);

spit out cash;

balance -= amount;

put_balance(account, balance);
release(lock);
spit out cash;
spit out cash;

What happens when green tries to acquire the lock?

Roadmap

Where we are eventually going:

The OS and/or the user-level thread package will provide some sort of efficient primitive for user programs to
utilize in achieving mutual exclusion (for example, locks or semaphores, used with condition variables)

There may be higher-level constructs provided by a programming language to help you get it right (for
example, monitors which also utilize condition variables)

But somewhere, underneath it all, there needs to be a way to achieve hardware mutual exclusion (for example,
test-and-set used to implement spinlocks)

This mechanism will not be utilized by user programs

But it will be utilized in implementing what user programs see

Spinlocks

How do we implement spinlocks? Heres one attempt:

struct lock_t {
int held = 0;
}
void acquire(lock) {
while (lock->held);
lock->held = 1;
Why doesnt this
} work?
where isvoid
release(lock)
{
the race
condition?
lock->held = 0;
}

the caller busy-waits,


or spins, for lock to be
released hence spinlock

Implementing spinlocks (cont.)

Problem is that implementation of spinlocks has critical sections, too!

the acquire/release must be atomic

atomic == executes as though it could not be interrupted

code that executes all or nothing

Need help from the hardware

atomic instructions

test-and-set, compare-and-swap,

disable/reenable interrupts

to prevent context switches

Spinlocks redux: Hardware Test-and-Set

CPU provides the following as one atomic instruction:

bool test_and_set(bool *flag) {


bool old = *flag;
*flag = True;
Remember, this is a single atomic instruction
return old;
}

Implementing spinlocks using Test-and-Set

So, to fix our broken spinlocks:

struct lock {
int held = 0;
}
void acquire(lock) {
while(test_and_set(&lock->held));
mutual exclusion?
(at most one thread in the critical section)
}
progress? (Tvoid
outside
cannot prevent {S from entering)
release(lock)
bounded waiting?
(waiting T will
eventually enter)
lock->held
= 0;
} (low overhead (modulo the spinning part ))
performance?

Reminder of use
acquire(lock)
balance = get_balance(account);

int withdraw(account, amount) {

balance -= amount;

balance = get_balance(account);
balance -= amount;
put_balance(account, balance);

critical
section

acquire(lock);

acquire(lock)
put_balance(account, balance);
release(lock);

release(lock);

balance = get_balance(account);

spit out cash;

balance -= amount;

put_balance(account, balance);
release(lock);
spit out cash;
spit out cash;

How does a thread blocked on an acquire (that is, stuck in a test-and-set loop) yield the CPU?

calls yield( ) (spin-then-block)

theres an involuntary context switch (e.g., timer interrupt)

Problems with spinlocks

Spinlocks work, but are wasteful!

if a thread is spinning on a lock, the thread holding the lock cannot make progress

(pthread_spin_t)

Only want spinlocks as primitives to build higher-level synchronization constructs

Youll spin for a scheduling quantum

Why is this okay?

Well see later how to build blocking locks

But there is overhead can be cheaper to spin

(pthread_mutex_t)

Another approach: Disabling interrupts


struct lock {
}
void acquire(lock) {
cli();
// disable interrupts
}
void release(lock) {
sti();
// reenable interrupts
}

Problems with disabling interrupts

Only available to the kernel

Cant allow user-level to disable interrupts!

Insufficient on a multiprocessor

Each processor has its own interrupt mechanism

Long periods with interrupts disabled can wreak havoc with devices

Just as with spinlocks, you only want to use disabling of interrupts to build higher-level synchronization constructs

Race conditions

Informally, we say a program has a race condition (aka data race) if the result of an executing depends on timing

i.e., is non-deterministic

Typical symptoms

I run it on the same data, and sometimes it prints 0 and sometimes it prints 4

I run it on the same data, and sometimes it prints 0 and sometimes it crashes

Summary

Synchronization introduces temporal ordering


Adding synchronization can eliminate races
Synchronization can be provided by locks, semaphores, monitors, messages
Spinlocks are the lowest-level mechanism
primitive in terms of semantics error-prone
implemented by spin-waiting (crude) or by disabling interrupts (also crude, and can only be done in the
kernel)
In our next exciting episode
semaphores are a slightly higher level abstraction
Importantly, they are implemented by blocking, not spinning
Locks can also be implemented in this way
monitors are significantly higher level
utilize programming language support to reduce errors

Questions or Suggestions

Thank You!
inquiry
dishcseATyahooDOTcom &
msjATewubdDOTedu

Potrebbero piacerti anche