chapter 28 locks chien-chung shen cis, ud [email protected]
TRANSCRIPT
Basic Ideas
• Problem on concurrent programming – like to execute a sequence of instructions atomically on single CPU with interrupts
• Solution: put locks around critical sections
• lock_t mutex; // some globally-allocated lock ’mutex’ … lock(&mutex); balance = balance + 1; // critlcal section unlock(&mutex);
Pthread Locks• pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; Pthread_mutex_lock(&lock); // wrapper for pthread_mutex_lock() balance = balance + 1; Pthread_mutex_unlock(&lock);
• Course-grained locking vs. fine-grained locking– “Long” vs. “short” critical sections– example with file access
Evaluating Locks
• Three criteria– Mutual exclusion - correctness– Fairness – avoid starvation– Performance - overhead
Controlling Interrupts
void lock () { disableInterrupt(); }
void unlock() { enable Interrupt(); }
Negatives:–Allow calling thread to perform privileged operation–Does not work on multiprocessors
First Attempt – use a variable
typedef struct __lock_t { int flag; } lock_t;
void init(lock_t *mutex) { mutex->flag = 0; // 0 -> lock is available, 1 -> held }void lock(lock_t *mutex) { while (mutex->flag == 1) // TEST the flag ; // spin-wait (do nothing) mutex->flag = 1; // now SET it! }void unlock(lock_t *mutex) { mutex->flag = 0; }
What problems does this solution have?
Code Interleaving
Thread 1 Thread 2 flag == 0 call lock()while (flag == 1)interrupt: switch to Thread 2
call lock() while (flag == 1) flag = 1; interrupt: switch to Thread 1 flag = 1; // set flag to 1 (too!)
• Problems:– correctness – no guarantee of mutual exclusion– Performance – spin-waiting
Test-and-Set
• Semantics int TestAndSet(int *ptr, int new) { int old = *ptr; // fetch old value at ptr *ptr = new; // store ’new’ into ptr return old; // return the old value }
• Returns the old value pointed to by ptr, and simultaneously updates said value to new
• Make “test” (of old lock value) and “set” (of new value) a single atomic operation
• SPARC – ldstub // load/store unsigned byte• x86 – xchg // atomic exchange
Spin Lock with Test-and-Set
typedef struct __lock_t { int flag; } lock_t;
void init(lock_t *mutex) { mutex->flag = 0; // 0 -> lock is available, 1 -> held }void lock(lock_t *mutex) { while (TestAndSet(&lock->flag, 1) == 1) // TEST the flag ; // spin-wait (do nothing) }void unlock(lock_t *mutex) { mutex->flag = 0; }
• As long as the lock is held by another thread, TestAndSet() will repeatedly return 1, and thus the calling thread will spin-wait
• What kind of scheduler do we need on single processor? – preemptive scheduler (interrupt threads via
timer)
Evaluation of Spin Lock
• Three criteria– Mutual exclusion – correctness
• yes– Fairness – avoid starvation
• no– Performance – overhead
• Bad on single CPU• Reasonably well on multiple CPUs,
assuming critical sections are short
Compare-and-Swap
• On x86 - compare-and-exchange cmpxchgl• Semanticsint CompareAndSwap(int *ptr, int expected, int new) { int actual = *ptr; if (actual == expected) *ptr = new; return actual;}
• Lockvoid lock(lock_t *lock) { while (CompareAndSwap(&lock->flag, 0, 1) == 1) ; // spin }
• More powerful than Test-and-Set
Ticket Lock with Fetch&Addint FetchAndAdd(int *ptr) { // semantics int old = *ptr; *ptr = old + 1; return old; }
typedef struct __lock_t { int ticket; int turn; } lock_t;
void lock_init(lock_t *lock) { lock->ticket = 0; lock->turn = 0; }
void lock(lock_t *lock) { int myturn = FetchAndAdd(&lock->ticket); // get a ticket while (lock->turn != myturn) ; // spin if not my turn}
void unlock(lock_t *lock) { FetchAndAdd(&lock->turn); // enable the next waiting thread}
• Anything good ?– ensure progress for all threads and fair
How to Avoid Spinning ?
• Need OS support, in addition to hardwarevoid init() { flag = 0; }void lock() { while (TestAndSet(&flag, 1) == 1) // TEST the flag yield(); // give up CPU and move to READY state}void unlock() { flag = 0; }
• Another overhead ? (think 100 threads)– context switching overhead
• Still one problem not solved – starvation
Sleeping Instead of Spinning
• Explicitly exert some control over who gets to acquire the lock next after the current holder releases it
• What “data structure” would you use?– queue
• park() – put calling thread to sleep• unpark() – wake up a thread
Queue and Yield/Wakeuptypedef struct __lock_t { int flag; int guard; queue_t *q; } lock_t;
void lock_init(lock_t *m) { m->flag = 0; m->guard = 0; queue_init(m->q); }
void lock(lock_t *m) { while (TestAndSet(&m->guard, 1) == 1) ; //acquire guard lock by spinning if (m->flag == 0) { m->flag = 1; // lock is acquired m->guard = 0; } else { queue_add(m->q, gettid()); // added to the lock’s queue m->guard = 0; park(); // put calling thread to sleep } }
void unlock(lock_t *m) { while (TestAndSet(&m->guard, 1) == 1) ; //acquire guard lock by spinning if (queue_empty(m->q)) m->flag = 0; // let go of lock; no one wants it else unpark(queue_remove(m->q)); // hold lock (for next thread!) m->guard = 0; }
Questionsvoid lock(lock_t *m) { while (TestAndSet(&m->guard, 1) == 1) ; //acquire guard lock by spinning if (m->flag == 0) { m->flag = 1; // lock is acquired m->guard = 0; } else { queue_add(m->q, gettid()); // added to the lock’s queuex: m->guard = 0;y: park(); // put calling thread to sleep } }
• Why is guard used?• Can x and y be swapped?void unlock(lock_t *m) { while (TestAndSet(&m->guard, 1) == 1) ; //acquire guard lock by spinning if (queue_empty(m->q)) m->flag = 0; // let go of lock; no one wants it else unpark(queue_remove(m->q)); // hold lock (for next thread!) m->guard = 0; }
• Why flag does not get set to 0 when another thread gets woken up?
– the waking thread does not hold the guard