V Lecture 25 — shared-memory concurrency and mutual exclusion
V traditional model: sequential programming
V what happens when this isn’t true?
* programming: need to divide work among multiple threads of execution and coordinate among them
V 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
V 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
V 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)
V what can we do with multiple cores?
V 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
V 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
V concurrency is about how to efficiently manage access to shared resources
* our focus
V cooking analogy
* in 142/143: a program is like a recipe for a cook who does one thing at a time
V parallelism: have lots of potatoes to slice?
* hire helpers, hand out potatoes and knives
* too many cooks, and you spend all your time coordinating
V 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
V why do we need concurrency?
V first, why might we need multiple threads?
V responsiveness
* response to GUI events in one thread while another is computing something
V processor utilization
* if a thread “goes to disk,” have something else to do
V failure isolation
* an exception or other problem in one thread doesn’t stop the others
V consider the following multithreaded scenarios
V processing different bank-account operations
* what if two threads change the account at the same time?
V using shared cache of previously retrieved results
* what if two threads try and insert the same result at the same time?
V 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);
}
V 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
V computing could be interleaved in such a way to cause problems
V 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);
}
V NO, a similar interleaving will still result in an incorrect balance
V negative balance now possible, why?
* interlace comparison to amount, write to balance sequentially
V mutual exclusion
V 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)
V 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
V 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!
V 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;
}
V we’ve only moved where the problem is:
V many possible solutions, we’ll talk about locks today
V 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
V 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
V 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
V potential problems?
V 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
V 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