|
|
|
Lecture 25 — shared-memory concurrency and mutual exclusion
|
|
|
|
|
traditional model: sequential programming
|
|
|
|
|
what happens when this isn’t true?
|
|
|
|
|
programming: need to divide work among multiple threads of execution and coordinate among them
|
|
|
|
|
algorithms: how can we structure computation to take advantage of multiple threads to improve performace
|
|
|
|
|
called speed-up or throughput (more work done per unit time)
|
|
|
|
|
data structures: may need to support simultaneous access by multiple threads
|
|
|
|
|
brief, simplified history
|
|
|
|
|
writing correct multithreaded programs is very difficult, so sequential programs have usually been preferred
|
|
|
|
|
this pretty much worked out 1980-2005 because computers got twice as fast every two years
|
|
|
|
|
over the last 10 years, this hasn’t happened
|
|
|
|
|
heat dissipation has become a serious problem
|
|
|
|
|
memory access is too slow
|
|
|
|
|
solution: multiple processors on one chip (multicore)
|
|
|
|
|
what can we do with multiple cores?
|
|
|
|
|
run multiple programs at the same time
|
|
|
|
|
can be done with a single processor using time slicing
|
|
|
|
|
do multiple things at once within a single program
|
|
|
|
|
parallelism vs concurrency
|
|
|
|
|
these definitions are not official or universal, but a good way to think about this
|
|
|
|
|
parallelism is about how to use extra resources (i.e., multiple processors) to solve problems faster
|
|
|
|
|
concurrency is about how to efficiently manage access to shared resources
|
|
|
|
|
our focus
|
|
|
|
|
cooking analogy
|
|
|
|
|
in 142/143: a program is like a recipe for a cook who does one thing at a time
|
|
|
|
|
parallelism: have lots of potatoes to slice?
|
|
|
|
|
hire helpers, hand out potatoes and knives
|
|
|
|
|
too many cooks, and you spend all your time coordinating
|
|
|
|
|
concurrency: lots of cooks making different things
|
|
|
|
|
only four stove burners
|
|
|
|
|
want all burners to be used as much as possible without spills or incorrect burner settings
|
|
|
|
|
why do we need concurrency?
|
|
|
|
|
first, why might we need multiple threads?
|
|
|
|
|
responsiveness
|
|
|
|
|
response to GUI events in one thread while another is computing something
|
|
|
|
|
processor utilization
|
|
|
|
|
if a thread “goes to disk,” have something else to do
|
|
|
|
|
failure isolation
|
|
|
|
|
an exception or other problem in one thread doesn’t stop the others
|
|
|
|
|
consider the following multithreaded scenarios
|
|
|
|
|
processing different bank-account operations
|
|
|
|
|
what if two threads change the account at the same time?
|
|
|
|
|
using shared cache of previously retrieved results
|
|
|
|
|
what if two threads try and insert the same result at the same time?
|
|
|
|
|
canonical example: bank account
|
|
|
|
|
void BankAccount::withdrawal(double amount) { double b = get_balance(); if (amount > b) { throw std::invalid_argument(“amount is greater than current balance”); } set_balance(b - amount); }
|
|
|
|
|
suppose thread T1 calls x.withdrawal(100) and thread T2 calls y.withdrawal(100)
|
|
|
|
|
we’re fine unless x and y happen to be the same account
|
|
|
|
|
If second call starts before first finishes, we say the calls interleave
|
|
|
|
|
computing could be interleaved in such a way to cause problems
|
|
|
|
|
does this fix it?
|
|
|
|
|
void BankAccount::withdrawal(double amount) { if (amount > get_balance()) { throw std::invalid_argument(“amount is greater than current balance”); } set_balance(get_balance() - amount); }
|
|
|
|
|
NO, a similar interleaving will still result in an incorrect balance
|
|
|
|
|
negative balance now possible, why?
|
|
|
|
|
interlace comparison to amount, write to balance sequentially
|
|
|
|
|
mutual exclusion
|
|
|
|
|
sane fix: allow at most one thread to withdraw from account A at a time
|
|
|
|
|
exclude other simultaneous operations on A too (e.g., deposit)
|
|
|
|
|
called mutual exclusion: one thread using a resource (here: an account) means another thread must wait
|
|
|
|
|
also called critical sections, which technically have other requirements
|
|
|
|
|
programmer must implement critical sections
|
|
|
|
|
the compiler has no idea what interleavings should or should not be allowed in your program
|
|
|
|
|
but you need language primitives to do it!
|
|
|
|
|
can we implement our own mutual exclusion?
|
|
|
|
|
void BankAccount::withdrawal(double amount) { while(busy) { } busy = true; double b = get_balance(); if (amount > b) { throw std::invalid_argument(“amount is greater than current balance”); } set_balance(b - amount); busy = false; }
|
|
|
|
|
we’ve only moved where the problem is:
|
|
|
|
|
many possible solutions, we’ll talk about locks today
|
|
|
|
|
a lock is an abstract datatype has the following operations
|
|
|
|
|
new: make a new lock, initially “not held”
|
|
|
|
|
acquire: blocks if this lock already “held”, otherwise makes lock “held”
|
|
|
|
|
release: make this lock “not held”, one currently blocked thread will acquire it
|
|
|
|
|
the lock implementation ensures this operations happen “all at once” (no possibility for other code to execute in the middle)
|
|
|
|
|
need special hardware or operating system support
|
|
|
|
|
almost there:
|
|
|
|
|
// BankAccount has void BankAccount::withdrawal(double amount) { lk.acquire(); double b = get_balance(); if (amount > b) { throw std::invalid_argument(“amount is greater than current balance”); } set_balance(b - amount); lk.release(); }
|
|
|
|
|
up to the programmer to use a lock correctly
|
|
|
|
|
potential problems?
|
|
|
|
|
other methods that write to balance will need to acquire the same lock
|
|
|
|
|
if set_balance and withdrawal don’t acquire the same lock, the result could be wrong
|
|
|
|
|
if they do acquire the same lock, then withdrawal would block forever when it tries to acquire a lock it already has
|
|
|
|
|
if we throw the exception, we never release the lock
|
|
|
|
|
re-entrant locks
|
|
|
|
|
we allow a thread that already has a lock to reacquire that lock
|
|
|
|
|
keep a count of how many times a lock has been reacquired
|
|
|
|
|
the lock must be released that many times before another thread can acquire it
|
|
|