Review session 4 Outline: 1. Deadlock 5min 2. Monitor 5min 3. Priority inversion 10min 4. Rwlock 10min 5. Sequential consistency 15min 1. Deadlock (5min) Deadlock is a situation in which two or more competing actions are each waiting for the other to finish, and thus neither ever does. HW4: Ask students in which situation there will be a deadlock. [Handout // assume all the variables are initialized correctly double balance[2]; // 0 for alice, 1 for bob smutex_t mtx[2]; // 0 for alice, 1 for bob bool transfer(int from, int to, double trans) { smutex_lock(&mtx[from]); smutex_lock(&mtx[to]); bool result = false; if (balance[from] > trans) { balance[from] = balance[from] - trans; balance[to] = balance[to] + trans; result = true; } smutex_unlock(&mtx[to]); smutex_unlock(&mtx[from]); return result; } ] 2. Monitor (5min) Function "payTax" will wait until there are enough money to be paid. Ask two students to finish two functions: deposit and payTax on board. Write the APIs on board for reference: scond_wait(scond_t *, smutex_t*); scond_signal(scond_t *, smutex_t*); scond_broadcast(scond_t *, smutex_t*); [Handout class BankAccount { // assume all the variables are initialized correctly private: scond_t condv; smutex_t mtx; double balance; public: bool withdraw(double amount); void deposit(double amount); void payTax(double amount); }; bool BankAccount::withdraw(double amount) { bool ret = false; smutex_lock(&mtx); if (balance >= amount) { balance -= amount; ret = true; } smutex_unlock(&mtx); return ret; } void BankAccount::deposit(double amount) { // your code here } void BankAccount::payTax(double amount) { // your code here } ] Answers should be: void BankAccount::deposit(double amount) { smutex_lock(&mtx); balance += amount; scond_broadcast(&condv, &mtx); // signal is not a good choice smutex_unlock(&mtx); } void BankAccount::payTax(double amount) { smutex_lock(&mtx); while(balance < amount) { // cannot be if scond_wait(&condv, &mtx); } balance -= amount; smutex_unlock(&mtx); } 3. Priority inversion (10min) Recall HW4, there are three tasks with high/medium/low priority. Higher priority tasks will preempt the lower priority tasks whenever they can. If one task cannot proceed, it will yield. Give all the starting sequence permutations (on board): HML (high -> medium -> low) HLM MHL MLH LHM LMH Ask three students to give the outputs for these 6 permutations. [Handout smutex_t res; void highPriority() { ... // do something smutex_lock(&res); ... // handle resource smutex_unlock(&res); printf("A "); } void mediumPriority() { ... // do something printf("B "); } void lowPriority() { smutex_lock(&res); ... // handle resource smutex_unlock(&res); ... // do something printf("C "); } ] 4. Reader writer lock (10min) - Atomic instructions (3min) Atomic instructions appear to the rest of the system to occur instantaneously. Atomicity is a guarantee of isolation from concurrent processes. cmpxchg_val(int* addr, int oldval, int newval) [on board] This is an atomic operation that compares oldval to *addr, and if the two are equal, it sets *addr = newval. It returns the old contents of *addr. atomic_decrement(int* arg) [on board] This atomically performs *arg = *arg - 1. - Ask students to interpret the answers (7min) [Handout struct sharedlock { int value; // when the lock is created, value is initialized to 0 }; void reader_release(struct sharedlock* lock) { atomic_decrement(&lock->value); } void writer_acquire(struct sharedlock* lock) { while(cmpxchg_val(&lock->value, 0, -1) != 0) {} } void writer_release(struct sharedlock*) { cmpxchg_val(&lock->value, -1, 0); } ] 5. Sequential consistency (15min) Sequential consistency means that all operations are interleaved according to *program order* on each CPU (in other words, the interleavings are those that we could get if the threads were running on a single CPU). the LACK of sequential consistency therefore means that we have interleavings and orderings that are not possible in a single CPU. - Break intuitiveness (5min) Sequential consistency does not hold in current widely-used commodity multi-core CPUs. A real example is the double-checked locking pattern in class. A simplified example would be the following. (Ask students to answer the question.) [Handout int a = 0, b = 0; void foo() { a = 1; b = 1; } void bar() { while (b == 0); printf("%d", a); } Question: Can the output be 0? ] - Explain in concrete way (5min) Draw two CPUs with its own cache and sharing the memory on board. Ask one student to simulate the process of output 0. (1) On CPU1, a = 1 on cache, then b = 1 on cache, a,b are 0 on memory. (2) On CPU2, spinning on b == 0. (3) On CPU1, b == 1 flush to memory (no sequential consistency guarantee). (4) On CPU2, program goes on, and print out a == 0. How can mutex help? (1) Mutex provides exclusive access (2) Mutex make sure that all the modification to memory have been "seen" by other cores. The reason why is mutex's implementation includes atomic instruction xchg(), via spinlock. (see lecture handout05) - Try to fix it (5min) Ask student if this is the right implementation. [Handout int a = 0, b = 0; smutex_t mtx; // initialized correctly void foo() { smutex_lock(&mtx); // new code a = 1; b = 1; smutex_unlock(&mtx); // new code } void bar() { while (b == 0); printf("%d", a); } Question: Can the output be 0? ] The answer is yes, b == 1,a == 0 can still be seen by other cores before unlock mutex. Emphasize Mike Dahlin's concurrency programming standards. (e.g. "Always grab lock at beginning of procedure and release it right before return")