Objectives
·
To
introduce the critical-section problem, to ensure the consistency of shared data
·
To
introduce the concept of an atomic
transaction
and describe
mechanisms to ensure atomicity
Background
·
Concurrent
access to shared data may result in data inconsistency
·
For Example:
To provide a solution to the consumer-producer problem that fills
all the buffers:
We
can do so by having an integer count that keeps track of the
number of full buffers:
§ Initially, count := 0.
§ It is incremented by the producer after it produces a new
buffer and
§
It is decremented by the consumer after it consumes a
buffer.
Producer
while (true) {
/* produce an item and put in nextProduced */
while (count = = BUFFER_SIZE)
; // do nothing
buffer [in] = nextProduced;
in = (in + 1) %
BUFFER_SIZE;
count++;
}
Consumer
while (true) {
while (count = = 0)
; // do nothing
nextConsumed
= buffer[out];
out = (out +
1) % BUFFER_SIZE;
count--;
/* consume the item in nextConsumed
}
Race Condition
· count++ could be implemented as
register1 = count
register1 = register1 + 1
count = register1
· count-- could be implemented as
register2 = count
register2 = register2 - 1
count = register2
· Consider this execution
interleaving with “count = 5” initially:
S0:
producer execute register1 = count {register1 = 5}
S1: producer execute register1 = register1 +
1 {register1 = 6}
S2: consumer execute register2 = count {register2 = 5}
S3: consumer execute register2 = register2 - 1 {register2 = 4}
S4: producer execute count = register1 {count = 6 }
S5: consumer execute count = register2 {count = 4}
ê
Mutual Exclusion
If process
Pi is executing in its
critical section, then no
other processes
can be
executing in their critical sections
Peterson’s Solution
· Two
process solution
· The two processes share two variables:
ü int turn;
ü boolean ready[2];
· The variable turn indicates whose turn is
to enter the critical section.
· The ready array is used to indicate if a process is
ready to enter the critical section.
ready[i] =
true implies that process Pi is ready!
Algorithm for Process Pi
do {
ready[i] = TRUE;
turn = j;
while (ready[j] && turn
== j) ;
critical section
ready[i] = FALSE;
remainder section
} while (TRUE);
Examples:
int
main(int argc,
char *argv[])
{
pthread_t tid0;
pthread_t tid1;
pthread_attr_t
attr;
int i;
pthread_attr_init(&attr);
pthread_create(&tid0, &attr, runner0, 0);
pthread_create(&tid1, &attr, runner1, 0);
pthread_join(tid0, NULL);
printf("thread 0 done\n");
pthread_join(tid1, NULL);
printf("thread 1 done\n");
}
void *runner0()
{
int i;
printf("Thread 0 Started\n");
for (;;)
{
for (i = 1; i <= 3; i++) {
printf("HI %d\n", i);
sleep(1);
}
}
}
void *runner1()
{
int i;
printf("Thread 1 Stareted\n");
for (;;)
{
for (i = 0; i <= 2; i++) {
printf("HI %c\n", i + 'a');
sleep(1);
}
}
}
To execute:
% twothreads
int turn;
int ready[2];
int
main(int argc,
char *argv[])
{
pthread_t tid0;
pthread_t tid1;
pthread_attr_t
attr;
int i;
pthread_attr_init(&attr);
pthread_create(&tid0, &attr, runner0, 0);
pthread_create(&tid1, &attr, runner1, 0);
pthread_join(tid0, NULL);
printf("thread 0 done\n");
pthread_join(tid1, NULL);
printf("thread 1 done\n");
}
void *runner0()
{
int i;
printf("Thread 0 Started\n");
for (;;)
{
ready[0] = TRUE;
turn = 1;
while (ready[1] && turn ==
1);
for (i = 1; i <= 3; i++) {
printf("HI %d\n", i);
sleep(1);
}
printf("\n");
ready[0] = FALSE;
}
}
void *runner1()
{
int i;
printf("Thread 1 Stareted\n");
for (;;)
{
ready[1] = TRUE;
turn = 0;
while (ready[0] && turn == 0);
for (i = 0; i <= 2; i++) {
printf("HI %c\n", i + 'a');
sleep(1);
}
printf("\n");
ready[1] = FALSE;
}
}
To execute:
% peterson
ê Synchronization
Hardware
· Many systems provide hardware support
for critical section code
· Modern machines provide special atomic hardware instructions (non-interruptible).
Examples:
ü Test and set memory value.
ü swap contents of two memory words.
Solution to Critical-section Problem Using
Locks
do {
acquire
lock
critical section
release
lock
remainder section
} while (TRUE);
ü TestAndSet
Instruction
boolean TestAndSet (boolean *target)
{
boolean rv = *target;
*target = TRUE;
return rv:
}
Solution
using TestAndSet
Shared boolean variable lock := FALSE;
.
do {
while ( TestAndSet (&lock ) ) ;
// critical section
lock = FALSE;
//
remainder section
} while (TRUE);
ü Swap Instruction
void Swap
(boolean *a, boolean *b)
{
boolean temp = *a;
*a
= *b;
*b
= temp:
}
Solution
using Swap
Shared Boolean variable lock := FALSE;
Each process
has a local Boolean variable key
do {
key = TRUE;
while ( key
== TRUE) Swap (&lock, &key );
critical section
lock = FALSE;
remainder section
}
while (TRUE);
ê Semaphore
Semaphore
S – integer variable
Two
standard operations modify S: wait (S) & signal
(S)
Originally
called (by the late Dijkstra): P & V
wait
(S) {
while ( S <= 0); /*busy wait*/
S--;
}
signal
(S) {
S++;
}
Mutual Exclusion
Semaphore mutex = 1;
do {
wait (mutex);
Critical Section
signal (mutex);
Remainder Section
} while (TRUE);
Semaphore implementation must guarantee that no two processes
can execute wait ()
and signal () on the same semaphore
at the same time.
ê Examples:
void
*functionC();
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_t
thread1, thread2;
int counter = 0;
int
lockone = 0;
int
lockall = 0;
int
nolock = 0;
main(int argc, char *argv[])
{
int rc1, rc2;
int mode;
if (argc == 1)
{
printf("Usage is ThreadMutex [0|1|2]\n");
printf("0 - no lock\n1 - lock one by one\n2 - lock all\n");
exit(0);
}
mode = atoi(argv[1]);
if (mode == 0) nolock = 1;
else if (mode == 1) lockone = 1;
else if (mode == 2) lockall = 1;
pthread_create(&thread1,
NULL, &functionC, NULL);
pthread_create(&thread2,
NULL, &functionC, NULL);
pthread_join(thread1,
NULL);
pthread_join(thread2,
NULL);
printf("Counter
value: %d\n", counter);
exit(0);
}
void *functionC()
{
int i;
if (lockall) {
pthread_mutex_lock(&mutex1);
for (i =
1; i <= 1000000; i++) {
counter++;
if (i
% 100000 == 0)
printf("
Thread (%d): i is: %d and counter is: %d \n",
pthread_self() -1, i, counter);
}
pthread_mutex_unlock(&mutex1);
}
else
if (lockone) {
for (i =
1; i <= 1000000; i++) {
pthread_mutex_lock(&mutex1);
counter++;
if (i
% 100000 == 0)
printf("
Thread (%d): i is: %d and counter is: %d \n",
pthread_self() -1, i, counter);
pthread_mutex_unlock(&mutex1);
}
} else
if (nolock) {
for (i =
1; i <= 1000000; i++) {
counter++;
if (i
% 100000 == 0)
printf("
Thread (%d): i is: %d and counter is: %d \n",
pthread_self() -1, i, counter);
}
}
}
To execute:
% ThreadMutex 0 (or 1 or 2)
public class
ThreadMutexJava extends Thread {
private static int mode = 1;
private static int countUp = 0;
private static int
threadCount = 0;
private int threadNumber = ++threadCount;
private static synchronized void syninc
() {
countUp++;
}
private static void inc
() {
countUp++;
}
public ThreadMutexJava()
{
System.out.println("Making " + threadNumber);
}
public void run() {
int i;
for (i=1;
i<= 1000000; i++) {
if (mode
== 1)
syninc();
else
inc();
if (i %
100000 == 0)
System.out.println
(" Thread#: " + threadNumber +
" i: " + i +
"
counter: " +
countUp);
}
}
public static void main(String[]
args) throws InterruptedException {
ThreadMutexJava.mode = Integer.parseInt(args[0]);
Thread
t1 = new ThreadMutexJava();
Thread
t2 = new
ThreadMutexJava();
t1.start();
t2.start();
System.out.println("All Threads Started");
t1.join();
t2.join();
System.out.println("All Threads Ended: " +
ThreadMutexJava.threadCount);
System.out.println("countUp final value is: " + ThreadMutexJava.countUp);
}
}
To execute:
% java ThreadMutexJava 0 (or 1 )
Semaphore Implementation with
no
busy waiting
· With each semaphore there is an
associated waiting queue.
· Each semaphore has two data items:
ü value (of type integer)
ü pointer to next process in the waiting queue.
· Two operations:
ü block – place the process invoking the
operation on the appropriate waiting
queue.
ü wakeup – remove one of processes in the
waiting queue and place it in the ready queue.
Implementation of wait:
wait (semaphore *S) {
S
-> value--;
if
(S -> value < 0) {
add this process to S -> list;
block();
}
}
Implementation of
signal:
signal (semaphore *S) {
S -> value++;
if
(S -> value <= 0) {
remove a process P from S ->list;
wakeup(P);
}
}
Deadlock and Starvation
·
Deadlock – two or more processes
are waiting indefinitely for an event that can be
caused by only one of the waiting processes
·
Let
S and Q be two semaphores
initialized to 1
P0 P1
wait
(S); wait (Q);
wait
(Q); wait (S);
. .
. .
. .
signal (S); signal (Q);
signal (Q); signal (S);
ê
Classical Problems of
Synchronization
II.
Readers-Writers
III.
Dinning-Philosophers
ü
N buffers, each can hold one item
ü
Semaphore
mutex initialized to the value 1
ü
Semaphore
full
initialized to the
value 0
ü
Semaphore
empty initialized to the value N.
The producer process
do
{
// produce an item
wait (empty);
wait (mutex);
// add the item to the buffer
signal (mutex);
signal (full);
}
while (TRUE);
The consumer process
do {
wait (full);
wait (mutex);
// remove an item
from buffer
signal (mutex);
signal (empty);
// consume the item
}
while (TRUE);
ê Examples:
pthread_mutex_t produce_mutex =
PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t consume_mutex =
PTHREAD_MUTEX_INITIALIZER;
int
b; /* buffer size = 1; */
main()
{
pthread_t producer_thread;
pthread_t consumer_thread;
void *producer();
void *consumer();
pthread_mutex_lock(&consume_mutex); /* to make sure that producer goes first */
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_join(consumer_thread, NULL);
}
add_buffer(int i)
{
b = i;
}
get_buffer()
{
return b;
}
producer()
{
int i;
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&produce_mutex);
add_buffer(i);
pthread_mutex_unlock(&consume_mutex);
printf("put
%d\n", i);
}
pthread_exit(NULL);
}
consumer()
{
int i, v;
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&consume_mutex);
v = get_buffer();
pthread_mutex_unlock(&produce_mutex);
printf("got
%d\n", v);
}
pthread_exit(NULL);
}
To execute:
%
ProducerConsumer
class
PCBuffer {
private int
contents;
private boolean available = false;
public synchronized int syncget() {
while
(available == false) {
try {
wait();
} catch(InterruptedException e) {}
}
available = false;
System.out.println("Consumer got: "
+ contents);
notifyAll();
return
contents;
}
public synchronized void syncput(int value) {
while
(available == true) {
try {
wait();
} catch(InterruptedException e) {}
}
contents = value;
available = true;
System.out.println("Producer put: " + contents);
notifyAll();
}
}
class Producer extends Thread {
private
PCBuffer Item;
public Producer(PCBuffer c) {
Item = c;
}
public void run() {
for (int i = 0; i < 10; i++) {
Item.syncput(i);
try {
sleep(1000);
}
catch(InterruptedException e) { }
}
}
}
class Consumer extends Thread {
private
PCBuffer Item;
public Consumer(PCBuffer c) {
Item = c;
}
public void run() {
int value = 0;
for (int
i = 0; i < 10; i++) {
value
= Item.syncget();
}
}
}
public class ProducerConsumerJava {
public static void main (String[]
args) {
PCBuffer Item = new
PCBuffer();
Producer p1 = new
Producer(Item);
Consumer c1 = new
Consumer(Item);
p1.start();
c1.start();
}
}
To execute:
% java
ProducerConsumerJava
#define
BUFFER_SIZE 10
/* The mutex lock */
pthread_mutex_t mutex;
/* the semaphores */
sem_t full;
sem_t
empty;
int b[BUFFER_SIZE];
int psleep, csleep;
main()
{
pthread_t producer_thread;
pthread_t consumer_thread;
void *producer();
void *consumer();
psleep = atoi(argv[1]);
csleep = atoi(argv[2]);
/* Create the mutex lock */
pthread_mutex_init(&mutex, NULL);
/* Create the full semaphore and initialize to 0 */
sem_init(&full, 0, 0);
/* Create the empty semaphore and initialize to BUFFER_SIZE */
sem_init(&empty, 0, BUFFER_SIZE);
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_join(consumer_thread, NULL);
}
int in = 0;
void add_buffer(int i)
{
b[in] = i;
in = (in + 1) % BUFFER_SIZE;
}
int out = 0;
int get_buffer()
{
int value;
value = b[out];
out = (out + 1) % BUFFER_SIZE;
return value;
}
producer()
{
int i = 0;
int ranvalue;
for (;;) {
ranvalue = 1 + (rand() % psleep);
sleep(ranvalue);
sem_wait(&empty); //acquire the empty lock
pthread_mutex_lock(&mutex);
//acquire the mutex lock
add_buffer(i); // CRITICAL SECTION
printf(">>put
%d\n", i++);
pthread_mutex_unlock(&mutex); //release the mutex
lock
sem_post(&full); //signal full
}
pthread_exit(NULL);
}
consumer()
{
int ranvalue;
int v;
for (;;) {
sem_wait(&full); //aquire
the full lock
pthread_mutex_lock(&mutex);
//aquire the mutex
lock
v = get_buffer(); //CRITICAL SECTION
printf("<<got %d\n", v);
pthread_mutex_unlock(&mutex);
//release the mutex lock
sem_post(&empty); //signal empty
ranvalue = 1 + (rand() % csleep);
sleep(ranvalue);
}
pthread_exit(NULL);
}
To execute:
%
ProducerConsumerSemaphore 5 5
%
ProducerConsumerSemaphore 5 4
%
ProducerConsumerSemaphore 4 5
· A data set is shared among
a number of concurrent processes
ü Readers – only read the data set;
they do not perform any updates
ü Writers – can both read and write
· Allow multiple readers to read at the same
time.
· Only one single writer can access the shared
data at the same time
· Shared Data
ü Data
set
ü Semaphore mutex initialized to 1
ü Semaphore wrt initialized to 1
ü Integer readcount initialized to 0
The Writer process
do {
wait (wrt)
writing is performed
signal (wrt) ;
} while (TRUE);
The Reader process
do {
wait (mutex) ;
readcount
++ ;
if
(readcount
== 1) //First Reader, keep writers out
wait (wrt) ;
signal (mutex)
reading
is performed
wait (mutex) ;
readcount -
- ;
if (readcount == 0) //Last Reader, allow in writers
signal (wrt) ;
signal
(mutex) ;
} while (TRUE);
III.
Dining-Philosophers Problem

Shared data
§
Bowl
of rice (data set)
§
Semaphore
chopstick [5] initialized to 1
Philosopher
i:
do {
wait ( chopstick[i] );
wait ( chopStick[ (i + 1) % 5] );
Eat
signal ( chopstick[i] );
signal (chopstick[ (i + 1) % 5] );
Think
} while (TRUE);
Examples:
#define
N 5
pthread_mutex_t
mymutex[N];
void
Pickup(int);
void Putdown(int);
philosopher(void *arg)
{
int sleeptime;
int i = pthread_self() - 1;
srand((unsigned)
time(0));
while (1) {
sleeptime
= 1 + rand() % N;
printf("\nPhilosopher %d is THINKING for %d
seconds\n", i, sleeptime);
sleep(sleeptime);
Pickup(i);
sleeptime
= 1 + rand() % N;
printf("\n >> Philosopher %d is EATING for %d seconds\n", i, sleeptime);
sleep(sleeptime);
Putdown(i);
}
}
Pickup(int i)
{
pthread_mutex_lock(&mymutex[(i) % N]);
usleep(slowdown);
pthread_mutex_lock(&mymutex[(i + 1) % N]);
}
Putdown(int i)
{
pthread_mutex_unlock(&mymutex[(i) % N]);
pthread_mutex_unlock(&mymutex[(i + 1) % N]);
}
main(int argc, char *argv[])
{
int rc, i;
int status;
pthread_t threads[N];
pthread_attr_t attr;
if (argc
> 1) slowdown = atoi(argv[1]);
for (i = 0; i < N; i++) {
pthread_mutex_init(&mymutex[i], NULL);
}
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
for (i = 0; i < N; i++) {
pthread_create(&threads[i], NULL, philosopher, NULL);
}
pthread_exit(NULL);
}
To execute:
%
DinningPhilosopher
To casuse deadlock:
%
DinningPhilosopher 5 // slowdown picking the 2nd
fork
class
Forks {
public int [] available = {1, 1, 1, 1, 1};
public synchronized void pickfork(int i) {
while
(available[i] == 0) {
try {
wait();
} catch(InterruptedException e) {
}
}
available[i] = 0;
}
public synchronized void putfork(int i) {
available[i] = 1;
notifyAll();
}
}
class Philosopher extends Thread {
private
Forks fks;
private int index;
private int
sleeptime;
static int slowdown = 0;
public Philosopher(Forks f, int number) {
fks = f;
index = number;
}
public void run() {
for (;;)
{
try
{
sleeptime = 1 + (int) (Math.random()
* 5);
System.out.println("\nPhilosopher
" + index +
"
is thinking for " + (int) sleeptime
+ " seconds\n");
sleep(sleeptime * 1000);
Pickup(index);
sleeptime = 1 + (int) (Math.random()
* 5);
System.out.println("\n >>
Philosopher " + index +
" is EATING for " + (int)
sleeptime + " seconds\n");
sleep(sleeptime * 1000);
Putdown(index);
}
catch(InterruptedException
e) {
}
}
}
public void Pickup(int i) {
fks.pickfork((i) % 5);
try {
sleep(slowdown * 1000);
} catch(InterruptedException e) {}
fks.pickfork((i + 1) % 5);
}
public void Putdown(int i) {
fks.putfork((i) % 5);
fks.putfork((i + 1) % 5);
}
}
public class DinningPhilosopherJava {
public static void main(String[] args) {
if (args.length == 1)
Philosopher.slowdown = Integer.parseInt(args[0]);
Forks fk = new
Forks();
for (int i = 0; i <= 4; i++) {
Philosopher p = new Philosopher(fk, i);
p.start();
}
}
}
To execute:
% java
DinningPhilosopherJava
To casuse deadlock:
% java
DinningPhilosopherJava 5 // slowdown picking 2nd fork
![]()
ê
Atomic
Transactions
· A collection of instructions or read /write
operations that performs single
logical function.
· Terminated by commit
(transaction successful) or abort (transaction failed) operation
·
Aborted transaction must be rolled back to undo any changes it performed
Types of Storage Media
· Volatile
storage – information
stored does not survive system crashes
Example: main memory, cache
· Nonvolatile
storage – Information
usually survives crashes
Example: disk and tape
· Stable
storage – Information
never lost
Not actually possible, so approximated via replication to devices with independent failure modes
Log-Based Recovery
· Record to stable storage information
about all modifications by a transaction
· Each
record describes single transaction write operation including:
ü Transaction name
ü Data item name
ü Old value
ü New value
·
<Ti starts> written to
log when transaction Ti starts
·
<Ti commits> written when Ti commits
Log-Based Recovery Algorithm
·
Using
the log, system can handle any volatile memory errors
ü Undo(Ti) restores value of all
data updated by Ti
ü Redo(Ti) sets values of all data
in transaction Ti to new values
·
Undo(Ti) and redo(Ti) must be idempotent
i.e., multiple executions must have the same result as one execution
·
If
system fails, restore state of all updated data via log
ü If log contains <Ti starts> without <Ti commits>, Undo(Ti)
ü If log contains <Ti starts> and <Ti commits>, Redo(Ti)
Checkpoints
·
Checkpoints
shorten log and recovery time.
·
Checkpoint scheme:
ü Output all log records
currently in volatile storage to stable storage
ü Output all modified data
from volatile to stable storage
ü Output a log record <checkpoint> to the log on stable
storage
·
Recovery
only includes Ti, such that Ti started executing before the most recent
checkpoint.
ê
Concurrent Transactions
Transaction execution must be equivalent to serial execution – serializability
Concurrency-control
algorithms
provide serializability
Consider two
data items A, B and Transactions T0
, T1
Schedule 1: T0 then T1

Schedule 2: Concurrent Serializable
Schedule

Locking Protocol to ensure serializabilty
·
Ensure
serializability by associating lock with each data item
·
Locks
ü Shared: Ti has
share lock on item Q, then Ti can:
read Q but not write Q
ü Exclusive: Ti has
exclusive lock on item Q, then Ti can: read
Q and write
Q
·
Require
every transaction on item Q acquire appropriate lock
using:
Two-phase Locking
Protocol:
Each
transaction issues lock and unlock requests in two phases
ü Growing
– obtaining locks
ü Shrinking – releasing locks