Synchronization
Definitions
- Critical Regions: Regions of code that can only have one thread enter at a time, or risk a data race
- Atomic Operations: Operations where the architecture guarantees the read and write to the memory will not have something run in between
Dangers
- Data Races
- Two concurrently running threads are in a data race if they
- access some shared data,
- at least one access is a write,
- and the result depends on who goes first.
- Can be avoided using mutual exclusive accesses (critical regions)
- Two concurrently running threads are in a data race if they
- Loss of liveness
- Deadlock: Occurs when all processes block, with each process waiting on the next. In this condition, no progress is made
- Starvation: Occurs when at least one process waits indefinitely
Approaches
- Mutual Exclusion (locks)
- Thread local data (
__thread
) - Thread specific data (
pthread_getspecific
) - Parameters
Mutex
- A data structure for achieving mutual exclusion
- A flag denoting availability
- A queue of threads that are waiting to acquire it
- Operations
- Acquire/lock a mutex (atomic operation, may block)
- If the mutex is available, set it to unavailable and return
- If the mutex is not available, add the calling thread to the queue and block that thread
- Release/unlock a mutex (anomic operation, no blocking)
- If the queue is empty, set to available
- If the queue is not empty, select one of the threads in the queue and unblock it
- Acquire/lock a mutex (atomic operation, may block)
Condition Variables
- Denotes the occurrence of a possible synchronization event
- Let the threads wait or sleep on a specific condition variable and upon wake up/signaling explicitly check the conditions and decide to go back to sleep or continue with the mutual exclusive updates
- All checks need to employ mutual exclusion to avoid data races
- A condition variable is associated with a mutual exclusion lock to avoid data races
- Implemented using a queue of thread ids
- Functions
pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *mutex)
- Block the current thread
- Inserts the id of the calling thread to the queue of c, unlocks mutex, and blocks the calling thread
pthread_cond_signal(c)
- Wakes up a single thread
- If the queue of c is not empty, removes a thread T from the queue, unblocks it, and T tries to acquire the mutex
pthread_cond_broadcast(c)
- Wakes up everyone
- If the queue of c is not empty, removes all the waiting threads form the queue, unblocks them, and each thread tries to acquire the mutex
Semaphores
- Array of locks, counter
- Queue: List of processes waiting for resources
- Operations
- Creation
- Created with a starting count
- The count is the capacity that it is able to support
- Down (decrement)
- Call when you want access
- If count > 0, decrement and continue
- Otherwise, block and add caller to the queue
- Up (increment)
- Call when you are done with access
- If the queue is empty, increment count and continue
- Otherwise, dequeue and unblock that process
- Creation
- Any thread with access to the semaphore can increment or decrement
- A binary semaphore is similar to a mutex but can be unlocked by any thread
semaphore.h
sem_wait
decrementssem_post
increments
Busy Waiting
- Infinite loop that keeps checking that a lock is available until it is
- Very inefficient because it hogs resources doing nothing
- Types
- Strict Alternation
- Round robin of access
- Issue: Processes cannot have to wait for the other process to lock and unlock before locking again
- Flagging Interest
- Aims to avoid excessive waits by having processes indicate wanting to enter before giving access
- Issue: If both processes flag before one gets access, there is a deadlock
- Peterson Solution
- Combines the flagging and alternation approaches
- If both are interested at the same time, take turns
- Strict Alternation
- Implementation: Atomic Locking with a Spin Lock
; Test and Set Lock
busy_lock:
tsl register, lock ; Copy lock, reg; set lock = 1
cmp register, #0 ; Was lock zero?
jnz busy_lock ; If not zero, jump to busy_lock
ret ; Return (critical region entered)
busy_unlock:
move lock, #0 ; Unlock
ret ; Return (critical region exited)
Shadow Copies
- Copy on Write
- Necessary when one process is writing to a shared structure and other processes are reading
- Process
- Make original data read-only (read-only lock)
- Make a copy of the original data
- Update copy with new information
- Change pointer to new copy (new reads will see the copy)
- Release old data (delete) once all readers are done
Barriers
- Require all processes to reach a specific point before they can continue
- Can be implemented in a language or manually using other synchronization primitives (such as condition variables)
Monitors
- Type of lock
- Locks a thread until another thread notifies it to continue
- Guarantee that at most one process enters the critical region at a time
- Implementation
- Require language support
- Can be explicit or implicit within a language
- Not based on shared memory to lock
- Use conditional variables provided by the programming language
Messaging
- Message passing enables synchronization
- Have a single server that manages locks, and clients that request it
- Used by distributed operating systems
Problems
Dining Philosophers
- Demonstrates the pattern of circular deadlock
- Setup
- Philosophers sitting around a table. They can all be thinking, eating, or waiting.
- Each philosopher gets one chop stick, but they need to borrow a chop stick from a thinking neighbor to be able to eat
- Deadlock occurs when each philosopher is waiting on the next, and no one is able to eat
- Challenges
- Concurrency: No assumptions may be made about how long any process eats (runs critical section) or thinks (runs non-critical section)
- Safety: Any two philosophers (processes) that are not (logically) adjacent can eat (run its critical section) simultaneously
- Liveness: A philosopher that is not eating cannot prevent another philosopher from eating
Solution 1: Break the circularity
- Impose a total order
- So chopstick 1 must be acquired before chopstick 2 which must be acquired before chopstick 3, etc.
- So two cannot be waiting on the same resource
- Easiest solution
Solution 2: Acquiring all resources at once
- Either get both chopsticks or do not get any (do not just hold onto one and wait for the second)
- Must know about the state of the resources
mutex critical;
semaphore philo[N](0); // binary semaphores
enum philo_state { HUNGRY, THINKING, EATING } state[N];
void doPhilosophy(int philoIdx) {
while (isPhilosophizing) {
thinkDeeply();
takeBoth(philoIdx);
eat();
dropBoth();
}
}
void takeBoth(int philoIdx) {
mutex_lock(&critical);
state[philoIdx] = HUNGRY;
testEats(philoIdx);
mutex_unlock(&critical);
// after unlock so other blocked processes can immediately be unlocked
sem_wait(philo[philoIdx]);
}
void dropBoth(int philoIdx) {
mutex_lock(&critical);
state[philo] = THINKING;
// run test eats for the surrounding philos
// to unblock them if they are waiting
testEats(leftIdx(philoIdx));
testEats(rightIdx(philoIdx));
mutex_unlock(&critical);
}
void testEats(int philoIdx) {
if (
state[philoIdx] == HUNGRY &&
state[leftIdx(philoIdx)] != EATING &&
state[rightIdx(philoIdx)] != EATING
) {
state[philoIdx] = EATING;
sem_post(philo[philoIdx]);
}
}
int leftIdx(i) { return (i + N – 1) % N; }
int rightIdx(i) { return (i + 1) % N; }
Producer-Consumer
- When one or more producers and one or more consumers act on the same finite data buffer
- Producers place data (write) in the buffer. Consumers remove data (read) from the buffer
- Challenges
- Concurrency
- Producers and consumers act in parallel
- Multiple producers/consumers should be able to read buffers at the same time
- No assumptions may be made about the speed of any process
- Safety
- Never can two producers write to the same entry at the same time
- Never can two consumers read from the same entry at the same time
- Never can a consumer read from an entry while a producer is writing to it
- Liveness
- No process not currently writing to or reading from an entry may prevent another process from finding an entry
- No process must wait indefinitely to obtain access to an entry as long as entries are available
- Correctness
- No entry shall be overwritten with a new item before the previous item is removed
- No empty entry shall be read by a consumer
- Concurrency
- Solution
- Lock entire buffer using semaphores
- Prevents corrupt shared variables, double-write, double-read, etc.
- However, the critical regions cause a bottleneck (which becomes more pronounced with multiple producers/consumers```
- Marking entries using semaphores
- Instead of locking entire buffer, lock individual entries
- Critical section is only for finding the next entry instead of also writing it (which is much quicker)
- Lock entire buffer using semaphores
void producerThread() {
dataType item;
int entry;
while (isProducing) {
item = produceItem();
entry = getEmpty();
buffer.insert(entry, item);
alertFull(entry);
}
}
void consumerThread() {
dataType item;
int entry;
while (isConsuming) {
entry = getFull();
item = buffer.remove(entry);
alertEmpty(entry);
consumeItem(item);
}
}
semaphore critical(1), empty(MAX_ENTRIES), full(0);
int getEmpty() {
int entry;
empty.down();
critical.down();
entry = buffer.findEmpty();
buffer.markBusy(entry);
critical.up();
return entry;
}
int getFull() {
int entry;
full.down();
critical.down();
entry = buffer.findFull();
buffer.markBusy(entry);
critical.up();
return entry;
}
void alertFull(int entry) {
critical.down();
buffer.markFull(entry);
critical.up();
full.up();
}
void alertEmpty(int entry) {
critical.down();
buffer.markEmpty(entry);
critical.up();
empty.up();
}
Reader-Writers
- One or more readers (which only read) and one or more writers (which may read before writing) working on a single database concurrently
- Difference from producer-consumer: only the writer modifies the data structure instead of both
- Challenges
- Concurrency
- Multiple readers may read the database at the same time
- No assumptions about speed
- Safety
- Never two or more writers can write at the same time
- Never may a reader read from the database while a writer is writing it
- Liveness
- No process not currently using the database may prevent another from accessing it
- No process not currently using the database may prevent another from accessing it
- Concurrency
- Solution
- Use semaphores to allow multiple readers and then start writing when none are reading
- Uses alpha locks (tentative write lock)
- Allow a writer to read while waiting for a write lock
- Prevents other writers from obtaining an alpha lock
semaphore_t rm; // reader lock
semaphore_t wm; // writer lock
void read() {
sem_wait(rm);
numReaders++;
if (numReaders == 1)
sem_wait(wm);
sem_post(rm);
/* read */
sem_wait(rm);
numReaders--;
if (numReaders == 0)
sem_post(wm);
sem_post(rm);
}
void write() {
sem_lock(wm);
/* write */
sem_unlock(wm);
}