Note: most of the points below are applicable to both threads AND processes. For brevity, only threads will be used.
Situations where a result depends on the order of which threads run are known as a race condition.
int count = 0
void increment() {
int t = count;
t++;
count = t;
}
void decrement() {
int t = count;
t--;
count = t;
}
Can produce one of -1, 0, 1 depending on exact execution sequence.
Issue arises when a thread uses a global variable while a different thread is changing it.
Critical regions are sections of the program where shared memory is accessed. Key to solving concurrency problems is to ensure no two threads are in the critical region at the same time.
Enter when lock == 0
, wait otherwise.
Problem: consider the following execution sequence on two threads:
while (TRUE) {
while (TRUE) {
while (lock != 0);
while (lock != 0);
lock = 1;
lock = 1;
critical();
critical();
lock = 0;
non_critical();
lock = 0;
non_critical();
}
}
Here, thread 2 sees critical region is available and moves on. But before setting the lock, thread 1 runs and also sees critical region is available. Both threads enter the critical region at the same time.
Problem arises because the operation of reading the lock is separate to the operation of setting the lock; this is not an atomic operation.
while (TRUE) {
while (turn != 0);
critical_region();
turn = 1;
noncritical_region();
}
while (TRUE) {
while (turn != 1);
critical_region();
turn = 0;
noncritical_region();
}
Solution works due to strict alternation, but has following problems:
Interrupts can be disabled before entering a critical region, and reenabled after leaving.
Pros:
Cons:
Test and set instruction is used to implement lock variables correct. Ensures process of reading and acquiring the lock happens in a single atomic operation.
Pros:
Cons:
Busy waiting is when a thread sits in a tight loop waiting for entry into a critical region. This approach has the problems:
typedef struct {
int count;
struct process *L;
} semaphore;
Assume two simple operations:
sleep
suspends the process that invokves it.wakup(P)
resumes the execution of a blocked process P
.We now define the atomic sempahore operations as such:
wait(S) {
S.count--;
if (S.count < 0) {
// add this process to S.L
sleep;
}
}
signal(S) {
S.count++;
if (S.count <= 0) {
// remove a process P from S.L
wakeup(P);
}
}
Poducer produces data items and stores the items in a buffer. Consumer takes the items out of the buffer and consumes them.
We must keep current count of items in the buffer and:
#define N = 4
sempahore mutex = 1;
semaphore empty = N;
semaphore full = 0;
prod() { cons() {
while (TRUE) { while (TRUE) {
item = produce(); wait(full);
wait(empty); wait(mutex);
wait(mutex); remove_item();
insert_item(); signal(mutex);
signal(mutex); signal(empty);
signal(full);
} }
} }
wait
: add calling thread to queue and put it to sleep.signal
: remove a thread from the queue and wake it up.
broadcast
: remove and wake-up all threads on the queue.