Locks 1
Locks 1
COMS(3010A)
Locks
Branden Ingram
[email protected]
Office Number : ???
Recap
• Concurrency
• Threads
• Thread API
Sharing Objects
• If a program has “independent threads” that operate on completely separate subsets of
memory, we can reason about each thread separately
Sharing Objects
• If a program has “independent threads” that operate on completely separate subsets of
memory, we can reason about each thread separately
• Threads which can read and write the shared state are called “Cooperating threads”
Sharing Objects
• If a program has “independent threads” that operate on completely separate subsets of
memory, we can reason about each thread separately
• Threads which can read and write the shared state are called “Cooperating threads”
• Threads which can read and write the shared state are called “Cooperating threads”
• The final value will depend on the order in which it was written
• Walk into a cafe and ask for a drink and a sandwich. The person behind the counter hands
you the sandwich (which is right next to him), then walks to the fridge to get your drink. Do
you care that he gave them to you in the "wrong" order? Would you rather he did the slow
one first, simply because that's how you gave the order?
• https://stackoverflow.com/questions/37725497/how-does-memory-reordering-help-
processors-and-compilers
How can we reason about thread interleavings when
compilers and hardware may reorder their operations?
• Walk into a cafe and ask for a drink and a sandwich. The person behind the counter hands
you the sandwich (which is right next to him), then walks to the fridge to get your drink. Do
you care that he gave them to you in the "wrong" order? Would you rather he did the slow
one first, simply because that's how you gave the order?
• https://stackoverflow.com/questions/37725497/how-does-memory-reordering-help-
processors-and-compilers
• A naive approach to these problems would be an ad hoc reasoning to the effect of process
interleaving
Thread A Thread B
x = x + 1; x = x + 2;
Interleaving 1
load r1,x
add r2,r1,1
store x,r2
load,r1,x
add,r2,r1,x
store x,r2
Final x=3
Challenges
• Race Conditions : Occurs when the behaviour of a program depends on the interleaving of
operations of different threads
Thread A Thread B
x = x + 1; x = x + 2;
Interleaving 1 Interleaving 2
load r1,x load r1,x
add r2,r1,1 load,r1,x
store x,r2 add r2,r1,1
load,r1,x add,r2,r1,x
add,r2,r1,x store x,r2
store x,r2 store x,r2
Thread A Thread B
x = x + 1; x = x + 2;
• During an atomic operation, a processor can read and write a location during the same
data transmission.
• An “atomic” operation is one which executes as if were not interrupted in space or time
• Load, Store
Challenges
• Too Much Milk : models the problem of coordinating access to shared memory by multiple
threads using only loads and stores
• Problem : two roommates share a fridge, these are good roommates and always make
sure the fridge is stocked with milk
Challenges
• Too Much Milk : models the problem of coordinating access to shared memory by multiple
threads using only loads and stores
• Problem : two roommates share a fridge, these are good roommates and always make
sure the fridge is stocked with milk
• Scenario :
• If the only atomic operations on shared state are atomic loads and stores to memory
• Is there a solution to the Too Much Milk Problem that satisfies :
• Safety – The program never enters a bad state
• Never more than one person buys milk
• Liveness – The program eventually enters a good state
• If milk is needed, someone eventually buys it
Challenges
If(milk==0){ // if no milk
if(note==0){ // if no note
note=1; // leave note
milk++; // buy milk
note=0; // remove note
}
}
If(milk==0){
If(milk==0){
if(note==0){
note=1;
milk++;
note=0;
}
}
if(note==0){
note=1;
milk++;
note=0;
}
}
Challenges
• Another idea is two have notes per thread which can be used to check if another is
contemplating buying milk
Thread 1 Thread 2
noteA=1; noteB=1;
if(noteB==0){ if(noteA==0){
if(milk==0){ if(milk==0){
milk++; milk++;
} }
} }
noteA=0; noteB=0;
Thread 1 Thread 2
noteA=1; noteB=1;
While(noteB==1){ if(noteA==0){
DoNothing; if(milk==0){
} milk++;
If(milk==0){ }
milk++; }
} noteB=0;
noteA=0;
Thread 1 Thread 2
noteA=1; noteB=1;
While(noteB==1){ if(noteA==0){
DoNothing; if(milk==0){
} milk++;
If(milk==0){ }
milk++; }
} noteB=0;
noteA=0;
Kitchen::buyIfNeeded(){
lock.acquire();
if(milk==0){
milk++;
}
lock.release();
}
• Properties
• Mutual Exclusion – At most one thread can hold a lock
• Progress – At some point some threads succeeds at obtaining a lock
• Bounded Waiting – waiting time is bounded by the number of threads
Locks
• Ensure that any critical section executes as if it were a single atomic instruction.
• lock()
• Try to acquire the lock.
• If no other thread holds the lock, the thread will acquire the lock.
• Enter the critical section.
• This thread is said to be the owner of the lock.
• Other threads are prevented from entering the critical section while the first thread
that holds the lock is in there
Pthread Locks - mutex
•Fairness
• Does each thread contending for the lock get a fair shot at acquiring it once it is free?
(Starvation)
•Performance
• The time overheads added by using the lock
Controlling Interrupts
• Disable Interrupts for critical sections
• One of the earliest solutions used to provide mutual exclusion
• Invented for single-processor systems.
1 void lock() {
2 DisableInterrupts();
3 }
4 void unlock() {
5 EnableInterrupts();
6 }
Controlling Interrupts
• Disable Interrupts for critical sections
• One of the earliest solutions used to provide mutual exclusion
• Invented for single-processor systems.
1 void lock() {
2 DisableInterrupts();
3 }
4 void unlock() {
5 EnableInterrupts();
6 }
• Problem:
• Require too much trust in applications
• Greedy (or malicious) program could monopolize the processor.
• Do not work on multiprocessors
• Code that masks or unmasks interrupts be executed slowly by modern CPUs
Why hardware support is needed?
• First attempt: Using a flag denoting whether the lock is held or not.
• The code below has problems.
Thread1 Thread2
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!)
Why hardware support is needed?
• Problem 1: No Mutual Exclusion (assume flag=0 to begin)
Thread1 Thread2
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!)
Thread1 Thread2
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!)
Fairness: no
Spin locks don’t provide any fairness guarantees.
Indeed, a thread spinning may spin forever.
Performance:
In the single CPU, performance overheads can be quite painful.
If the number of threads roughly equals the number of CPUs, spin locks work reasonably
well.
Compare-And-Swap
• Test whether the value at the address(ptr) is equal to expected.
• If so, update the memory location pointed to by ptr with the new value.
• In either case, return the actual value at that memory location.
1 void init() {
2 flag = 0;
3 }
4
5 void lock() {
6 while (TestAndSet(&flag, 1) == 1)
7 yield(); // give up the CPU
8 }
9
10 void unlock() {
11 flag = 0;
12 }
• Queue to keep track of which threads are waiting to enter the lock.
• park()
• Put a calling thread to sleep
• unpark(threadID)
• Wake a particular thread as designated by threadID.
Using Queues: Sleeping Instead of Spinning
1 typedef struct __lock_t { int flag; int guard; queue_t *q; } lock_t;
2
3 void lock_init(lock_t *m) {
4 m->flag = 0;
5 m->guard = 0;
6 queue_init(m->q);
7 }
8
9 void lock(lock_t *m) {
10 while (TestAndSet(&m->guard, 1) == 1)
11 ; // acquire guard lock by spinning
12 if (m->flag == 0) {
13 m->flag = 1; // lock is acquired
14 m->guard = 0;
15 } else {
16 queue_add(m->q, gettid());
17 m->guard = 0;
18 park();
19 }
20 }
21 …
1 queue_add(m->q, gettid());
2 setpark(); // new code
3 m->guard = 0;
4 park();
Code modification inside of lock()
Futex
• Linux provides a futex (is similar to Solaris’s park and unpark).
• futex_wait(address, expected)
• Put the calling thread to sleep
• If the value at address is not equal to expected, the call returns immediately.
• futex_wake(address)
• Wake one thread that is waiting on the queue.
Futex
• Snippet from lowlevellock.h in the nptl library
• The high bit of the integer v: track whether the lock is held or not
• All the other bits : the number of waiters
16 if (v >= 0)
17 continue;
18 futex_wait(mutex, v);
19 }
20 }
21
22 void mutex_unlock(int *mutex) {
23 /* Adding 0x80000000 to the counter results in 0 if and only if
24 there are not other interested threads */
25 if (atomic_add_zero(mutex, 0x80000000))
26 return;
27 /* There are other threads waiting for this mutex,
28 wake one of them up */
29 futex_wake(mutex);
30 }
A two-phase lock realizes that spinning can be useful if the lock is about to be
released.
Two-Phase Locks
A two-phase lock realizes that spinning can be useful if the lock is about to be
released.
First phase
The lock spins for a while, hoping that it can acquire the lock.
If the lock is not acquired during the first spin phase, a second phase is
entered
Two-Phase Locks
A two-phase lock realizes that spinning can be useful if the lock is about to be
released.
First phase
The lock spins for a while, hoping that it can acquire the lock.
If the lock is not acquired during the first spin phase, a second phase is
entered,
Second phase
The caller is put to sleep.
The caller is only woken up when the lock becomes free later.