|
|
|
Lecture 27 — data races and memory reordering, deadlock, reader/writer locks, condition variables
|
|
|
|
|
critical section granularity
|
|
|
|
|
how much work should be done while holding the lock?
|
|
|
|
|
too long = performance loss
|
|
|
|
|
too short = bugs
|
|
|
|
|
guideline #3: do not do expensive computations or I/O in critical sections, but also don’t introduce race conditions
|
|
|
|
|
data races
|
|
|
|
|
class C { private: int x = 0; int y = 0; public: void f() { x = 1; y = 1; } void g() { int a = y; int b = x; assert(b >= a); } }
|
|
|
|
|
exercise: can the assert ever fail due to an interleaving?
|
|
|
|
|
Proof #1: exhaustively consider all possible orderings of access to shared memory (there are 6)
|
|
|
|
|
Proof #2: if !(b>=a), then a==1 and b==0. But if a==1, then y=1 happened before a=y. Because programs execute in order: a=y happened before b=x and x=1 happened before y=1. So by transitivity, b==1. Contradiction.
|
|
|
|
|
not so fast!
|
|
|
|
|
this code has a dace race (two actually), and therefore the assert can fail
|
|
|
|
|
why?
|
|
|
|
|
for performance reasons the compiler and hardware are allowed to reorder memory operations
|
|
|
|
|
compilers do optimizations that make code faster without changing the meaning
|
|
|
|
|
this is pretty much essential for reasonable performance
|
|
|
|
|
hardware isn’t actually just one nice single chunk of memory
|
|
|
|
|
various caches and buffers will allow some memory to be accessed faster
|
|
|
|
|
some memory operations might become “visible” in a different order than in the code
|
|
|
|
|
have the compilers and hardware gone mad?!
|
|
|
|
|
the guarantee:
|
|
|
|
|
the programmer promises not to write data races
|
|
|
|
|
the compiler/hardware will never perform a memory reordering that affects the result of a data-race-free multi-threaded program
|
|
|
|
|
the fix:
|
|
|
|
|
class C { private: int x = 0; int y = 0; std::mutex mix; public: void f() { mtx.lock(); x = 1; mtx.unlock(); mtx.lock(); y = 1; mtx.unlock(); } void g() { mtx.lock(); int a = y; mtx.unlock(); mtx.lock(); int b = x; mtx.unlock(); assert(b >= a); } }
|
|
|
|
|
more realistic example
|
|
|
|
|
class C { boolean stop = false; void f() { while(!stop) { // draw a monster } } void g() { stop = didUserQuit(); } }
|
|
|
|
|
there is a data race on stop
|
|
|
|
|
may not ever exit loop when user quits
|
|
|
|
|
will probably be ok in practice, but no guarantee!
|
|
|
|
|
deadlock
|
|
|
|
|
multiple threads are all blocked waiting for each other
|
|
|
|
|
if drawn as a graph, there is a cycle of waiting
|
|
|
|
|
motivating example
|
|
|
|
|
void transferTo(double amt, BankAccount a) { mtx.lock(); this.withdraw(amt); a.deposit(amt); mtx.unlock() }
|
|
|
|
|
withdraw also guarded by lock
|
|
|
|
|
dining philosophers
|
|
|
|
|
n philosophers are seated around a table, each with a plate of spaghetti
|
|
|
|
|
on the table in between each philosopher is a fork
|
|
|
|
|
in order to eat their spaghetti, a philosopher needs to have a fork in each hand
|
|
|
|
|
exercise: what protocol should the philosophers follow so that they all get to eat?
|
|
|
|
|
should be independent of who is at the table
|
|
|
|
|
number the forks, a philosopher must be holding the lower number fork in order to pick up the higher number
|
|
|
|
|
a resource hierarchy solution
|
|
|
|
|
back to bank account example
|
|
|
|
|
can you see how the dining philosophers solution could apply here?
|
|
|
|
|
options for deadlock-proof transfer:
|
|
|
|
|
make a smaller critical section: transferTo not synchronized
|
|
|
|
|
exposes intermediate state after withdraw before deposit
|
|
|
|
|
may be okay, but exposes wrong total amount in bank
|
|
|
|
|
coarsen lock granularity: one lock for all accounts allowing transfers between them
|
|
|
|
|
works, but sacrifices concurrent deposits/withdrawals
|
|
|
|
|
give every bank-account a unique number and always acquire locks in the same order (resource hierarchy)
|
|
|
|
|
entire program should obey this order to avoid cycles
|
|
|
|
|
code acquiring only one lock can ignore the order
|
|
|
|
|
another example
|
|
|
|
|
Java standard library’s StringBuffer
|
|
|
|
|
synchronized append(StringBuffer sb) { int len = sb.length(); if(this.count + len > this.value.length) this.expand(…); sb.getChars(0,len,this.value,this.count); }
|
|
|
|
|
exercise: spot the problem
|
|
|
|
|
lock for sb not held between call to sb.length and sb.getChars
|
|
|
|
|
sb could get longer in the meantime, causing append to throw ArrayBoundsException
|
|
|
|
|
if append tried to hold the lock, deadlock could occur, just like in transferTo
|
|
|
|
|
not easy to fix both problems without extra copying:
|
|
|
|
|
do not want unique ids on every StringBuffer
|
|
|
|
|
do not want one lock for all StringBuffer objects
|
|
|
|
|
actual Java library: fixed neither (left code as is; changed javadoc)
|
|
|
|
|
up to clients to avoid such situations with own protocols
|
|
|
|
|
other mechanisms for shared-memory concurrency
|
|
|
|
|
reader/writer locks
|
|
|
|
|
like a mutex, but a thread can acquire it in reader mode or writer mode
|
|
|
|
|
any number of reader mode threads can hold the lock
|
|
|
|
|
only one thread can hold the lock in writer mode, no readers allowed
|
|
|
|
|
useful when reads are common and writes are rare
|
|
|
|
|
a shared hashtable has lots of simultaneous lookups, infrequent inserts
|
|
|
|
|
condition variables
|
|
|
|
|
consider the scenario where threads are sharing a bounded buffer (queue with a fixed size)
|
|
|
|
|
some threads (producers) enqueue units of work into the buffer
|
|
|
|
|
other threads (consumer) dequeue units of work from the buffer and process them
|
|
|
|
|
what should a producer thread do if the queue is full?
|
|
|
|
|
what should a consumer thread do if the queue is empty?
|
|
|
|
|
they could use busy-waiting, just sitting in a while loop checking the condition
|
|
|
|
|
better if they just wait (i.e., sleep) until they are notified that they can proceed
|
|
|
|
|
a condition variable is an object with three operations
|
|
|
|
|
wait: thread atomically releases lock and blocks
|
|
|
|
|
notify: wake up one waiting thread (it will then try and reacquire the lock)
|
|
|
|
|
notifyAll: wake up all waiting threads
|
|
|