Content
- Concurrent Programming
- Mutual Exclusion
- Synchronization
Concurrent Programming
Threadscooperate to perform a common task by sharing data (global vars & heap data)- Problems
- Race conditions
Thread interleavingcause incorrect results- Access to shared variable must be exclusive
- Synchronization
- Ordering between threads must be enforced
- Race conditions
Mutual Exclusion
Atomic operation- Operation appears indivisible; rest of the system either doesn't observe any of the effects, or all of them
- Other threads may still run, but they should not observe any intermediate states of the operation
Mutual exclusion- Only one thread can read/update shared variables at a time
Critical section- The code region where
mutual exclusionis enforced
- The code region where
Mutex Lock
Critical sectionsaccessed in betweenlock,unlock- Functions
mutex = lock_create()lock_destroy(mutex)lock(mutex)- If lock free: acquire lock
- Else: wait/sleep until lock free
unlock(mutex)- Release lock
- Wake up one of the waiting threads
Mutual exclusionconditions- NO 2 threads in
critical sectionat the same time - NO assumption on speed of thread execution
- Thread running outside
critical sectionCANNOT block another thread - NO thread must wait forever to enter its
critical section
- NO 2 threads in
Mutex Implementation
Variable tracking
lock(l) { while (l == TRUE) ; l = TRUE; } unlock(l) { l = FALSE; }lis also a shared variable, solock&unlockshould beatomic
Make
lockatomiclock(l) { disable_interupts; while (l == TRUE) ; l = TRUE; } unlock(l) { l = FALSE; enable_interupts; }Works only on single core
Multiprocessor H/W provides
atomic instructionsTest-and-setlock,compare-and-swaplockOperate on a memory word, perform 2 operations indivisibly by CPU requesting the lock controller to lock out the memory location
int tset(int *lock) { // atomic in H/W int old = *lock; *lock = 1; ___> atomic return old; _| }
Spin locksUses
tsetin a loop// *l init to 0 lock(int *l) { while (tset(1)) ; } unlock(int *l) { *l = FALSE; }- Efficient only if
critical sectionsare short - But
CPUperforms no useful working waiting in the loop -> waste CPU
Yielding locks// *l init to 0 lock(int *l) { while (tset(1)) thread_yield(); } unlock(int *l) { *l = FALSE; }- But scheduler determines when it returns back -> delay lock acquire
Blocking locksUnlike 3., 4. polling for locks, now
unlockwill wakeup threads sleeping inlock// *l init to 0 lock(int *l) { while (tset(1)) thread_sleep(); } unlock(int *l) { *l = FALSE; thread_wakeup(); }- Access shared
ready queue, i.e. we need locking while implementing blocking
Locking solutions
Multiprocessor
blocking lock # manipulate queues ↓ spin lock # loop on atomic instruction ↓ atomic instruction # single instructionUniprocessor
blocking lock ↓ interrupt disabling
Which lock to use?
| Lock | When to use? |
|---|---|
| Atomic instruction | Most efficient, use when available |
| Interrupt disabling, spin locks | Critical sections short & will not block |
| Blocking locks | Critical sections long & may block (for synchronization); overhead for context switch |
How many locks to create?
- 1 per individual data structure
- More locks -> more parallelism BUT more bugs
Why do we need
spin lockwhen implementingblocking lock?- Avoid
lost wakeupbetween checking availability of lock & sleep - Need another locking method to protect the shared ready queue
- Avoid
Deadlocks, Starvation, Livelock
Deadlock: a set of threads each waiting for a resource held by another thread- Conditions
- Mutual exclusion
- Hold & wait
- No premption
- Circular wait
- Prevention
- Release previously acquired locks? (hold & wait)
- But modifications to data might have already happened
- Or
livelockwhen keep trying to acquire locks
- Number each resources, need to acquire lower numbered resources before higher ones? (circular wait)
- Difficult to number a whole bunch, and some of them are from third-party
- Release previously acquired locks? (hold & wait)
- Conditions
Starvation: a set of threads waiting for resources constantly used by othersLivelock: a set of threads continue to rum but make no progress- e.g.
interrupt livelock: interrupts queueing up and suspend the running threads => Can turn topolling, with intervals not too long - Need to ensure a thread runs for a while before switching
- e.g.
Synchronization
Threadswait on some conditions before proceeding
Motivation: Producer-Consumer Problem
Single producer & consumer
char buf[8]; int in; int out; void send(char msg) { while ((in-out+n) % n == n-1) ; // full buf[in] = msg; in = (in+1) % n; } char receive() { while (in == out) ; // empty msg = buf[out]; out = (out+1) % n; return msg; }Multiple producers & consumers
# deadlock void send(char msg) { lock(l); while ((in-out+n) % n == n-1) ; // full buf[in] = msg; in = (in+1) % n; unlock(l); } char receive() { lock(l); while (in == out) ; // empty msg = buf[out]; out = (out+1) % n; unlock(l); return msg; }# release locks before spinning - too tight void send(char msg) { lock(l); while ((in-out+n) % n == n-1) { unlock(l); lock(l); } buf[in] = msg; in = (in+1) % n; unlock(l); } char receive() { lock(l); while (in == out) { unlock(l); lock(l); } msg = buf[out]; out = (out+1) % n; return msg; unlock(l); }# sleep after unlocking void send(char msg) { lock(l); while ((in-out+n) % n == n-1) { unlock(l); ____> atomic! Or else lost wakeup thread_sleep(full); __| lock(l); } buf[in] = msg; if (in == out) thread_wakeup(empty); in = (in+1) % n; unlock(l); } char receive() { lock(l); while (in == out) { unlock(l); thread_sleep(empty); lock(l); } msg = buf[out]; if ((in-out+n) % n == n-1) thread_wakeup(full); out = (out+1) % n; unlock(l); return msg; }- Challenges
- Can't spin or sleep while holding lock
Deadlock
- Can't release lock and then sleep
Lost wakeup
- Need to unlock & sleep atomically
- Can't spin or sleep while holding lock
Monitors
Condition variable- Used within
monitor methods - Functions
cv = cv_create()cv_destroy(cv)cv_wait(cv, lock)- Lock released
atomicallywhile waiting - Lock reacquired after wait returns
- Lock released
cv_signal(cv, lock)- Wakeup one thread waiting on the condition
Lost signal: signal occurs before a wait
cv_broadcast(cv, lock)- Wakeup all threads waiting on the condition
- Used within
Basis
F() { int disabled = disaple_interrupt; do_work(); enable_interrupt(disabled); } do_work(){ int disabled = disaple_interrupt; // already disabled! ... enable_interrupt(disabled); // stay disabled }Producer-consumerwithmonitorsbuf[n], in, out; lock l = 0; cv full; cv empty; void send(char msg) { lock(l); while ((in-out+n) % n == n-1) { // put thread into wait queue for 'full' condition wait(full, l); // unlock + sleep + lock } buf[in] = msg; if (in == out) signal(empty, l); in = (in+1) % n; unlock(l); } char receive() { lock(l); while (in == out) { wait(empty, l); } msg = buf[out]; if ((in-out+n) % n == n-1) signal(full, l); out = (out+1) % n; unlock(l); return msg; }Variable initializationwithmonitorsMethod1() { lock(l); V = malloc(...); signal(cv, l); // condition = V is null ... unlock(l); } Method2() { lock(l); if (V == NULL) wait(cv, l); // condition = V is null assert(V); unlock(l); }Lockis for avoidinglost wakeupbecausesignalCANNOT come beforewait
Semaphores
Tracks number of available resources using
down&up# spinning (example with race condition on s) down() { while (s <= 0) ; s--; } up() { s++; }# blocked down() { disable_interrupts; while (s <= 0) thread_sleep(s); s--; enable_interrupts; } up() { disable_interrupts; s++; thread_wakeup(s); enable_interrupts; }- If
sinit to1, then behaves likelock&unlock
- If
Difference with
lock- Different threads call
down&up upcan happen beforedownto bank resource
- Different threads call
Producer-consumerwithsemaphoresbuf[n], in, out; lock l; sem full = 0; sem empty = n; void send(char msg) { down(empty); lock(l); buf[in] = msg; in = (in+1) % n; unlock(l); up(full); } char receive() { down(full); lock(l); // this lock for buf, not for avoiding lost signal msg = buf[out]; out = (out+1) % n; unlock(l); up(empty); return msg; }
Quick Questions
- What is the difference between mutual exclusion and synchronization?
- Mutex: used to ensure that only one thread accesses a critical section at a time, ensuring that operations are run atomically.
- Synchronization: used to ensure threads wait on some condition.
- Why are locks, by themselves, not sufficient for solving synchronization problems?
Lock&unlockare used together and in that order. Synchronization problems require a more general primitive: conditionalsleepandwakeup.
- What are the differences between a monitor and a semaphore?
Monitorrequires locks to avoid lost signal.Monitoris associated with a lock;semaphoreonly use locks to protect the shared resource.- No lost wakeups for
semaphorebecause the wakeups (up()) will not be dedicated to a specific thread but instead used for future entries. Down/Up&wait/signalare also different.
- What are the differences between
wait()anddown()?Waitis stateless, it always waits.Waitalso releases a lock, waits, and then reacquires a lock.Downhas state embedded with a notion of available resources, and will only wait if resources are not available.Downdoesn’t have any notion of an associated lock.
- What are the differences between
signal()andup()?Signalcan be lost if no one is waiting, hence the need to use locks with condition variables, so that there is no race withwait.Upwill always increase the resource available, so a future down can acquire the resource.
- Why might you prefer one over the other?
Semaphore: resource counting problemMonitor: other problems, ensures mutual exclusion