CSE 451
Operating Systems
L8 - Synchronization
Slides by: Tom Anderson
Baris Kasikci
Midterm Feedback
50%+ participation -> 1 extra day on the pset
75%+ participation -> 2 extra days on the pset
xk swtch (swtch.S)
swtch:
// callee saved registers already saved
// ptr to old PCB is in rdi push %rbp
push %rbx
push %r11
push %r12
push %r13
push %r14
push %r15
mov %rsp, (%rdi)
// ptr to new PCB is in rsi mov %rsi, %rsp
pop %r15 pop %r14 pop %r13 pop %r12 pop %r11 pop %rbx pop %rbp ret
xk swtch (swtch.S): Switch from A -> B
swtch:
// callee saved registers already saved
// ptr to old PCB is in rdi
push %rbp
push %rbx
push %r11
push %r12
push %r13
push %r14
push %r15
mov %rsp, (%rdi)
// ptr to new PCB is in rsi mov %rsi, %rsp
pop %r15 pop %r14 pop %r13 pop %r12 pop %r11 pop %rbx pop %rbp ret
Step 1: Save old thread’s preserved state (A)
xk swtch (swtch.S): Switch from A -> B
swtch:
// callee saved registers already saved
// ptr to old PCB is in rdi
push %rbp
push %rbx
push %r11
push %r12
push %r13
push %r14
push %r15
mov %rsp, (%rdi)
// ptr to new PCB is in rsi mov %rsi, %rsp
pop %r15 pop %r14 pop %r13 pop %r12 pop %r11 pop %rbx pop %rbp ret
Step 2: Record A’s execution point
current stack pointer
destination register (memory field inside the TCB)
xk swtch (swtch.S): Switch from A -> B
swtch:
// callee saved registers already saved
// ptr to old PCB is in rdi
push %rbp
push %rbx
push %r11
push %r12
push %r13
push %r14
push %r15
mov %rsp, (%rdi)
// ptr to new PCB is in rsi mov %rsi, %rsp
pop %r15 pop %r14 pop %r13 pop %r12 pop %r11 pop %rbx pop %rbp ret
Step 3: Switch to the new thread’s (B) stack
xk swtch (swtch.S): Switch from A -> B
swtch:
// callee saved registers already saved
// ptr to old PCB is in rdi
push %rbp
push %rbx
push %r11
push %r12
push %r13
push %r14
push %r15
mov %rsp, (%rdi)
// ptr to new PCB is in rsi mov %rsi, %rsp
pop %r15 pop %r14 pop %r13 pop %r12 pop %r11 pop %rbx pop %rbp ret
Step 4: Restore B’s state and resume it
A Subtlety
=> Set up newly created thread so that swtch will ”resume” at start of
thread
Stack Progression
Why is there a stub at all?
Why is there a stub at all?
What Happens After Switch?
After F calls yield
Timer Interrupt -> swtch
Timer Interrupt -> swtch
Timer Interrupt -> swtch
Timer Interrupt -> swtch
Two Threads Call Yield
Synchronization
Motivation
Question: Can this panic?
Thread 1 Thread 2
p = someComputation(); pInitialized = true;
while (!pInitialized)
;
q = someFunction(p);
if (q != someFunction(p)) panic
Definitions
Race condition: output of a concurrent program depends on the order of operations between threads
Data race: when a thread can read or modify a memory location while another thread may be modifying it. (2 threads, 1 is a write, no intervening sycnrhonization)
Memory Fence: hardware instruction to enforce ordering
Mutual exclusion: only one thread does a particular thing at a time
Lock: prevent someone from doing something
Data Races in C++
C++ has undefined semantics when there is a data race (since 2006)
⇒ compiler optimizes code assuming no data races
Processor architectures have non-intuitive memory semantics
Write buffering: next instruction executes while write is being completed
Too Much Milk Example
| Person A | Person B |
12:30 | Look in fridge. Out of milk. | |
12:35 | Leave for store. | |
12:40 | Arrive at store. | Look in fridge. Out of milk. |
12:45 | Buy milk. | Leave for store. |
12:50 | Arrive home, put milk away. | Arrive at store. |
12:55 | | Buy milk. |
1:00 | | Arrive home, put milk away. Oh no! |
Too Much Milk, #1
if (!note)
if (!milk) {
leave note buy milk remove note
}
Too Much Milk, #2
Thread A
leave note A if (!note B) {
if (!milk) buy milk
}
remove note A
Thread B
leave note B if (!noteA) {
if (!milk) buy milk
}
remove note B
Too Much Milk, #3
Thread A
leave note A
while (note B) // X
do nothing; if (!milk)
buy milk;
remove note A
Thread B
leave note B
if (!noteA) { // Y if (!milk)
buy milk
}
remove note B
Can guarantee at X and Y that either:
Lessons
Locks
Lock::acquire: wait until lock is free, then take it Lock::release: release lock, waking up anyone waiting for it
Question: Why only Acquire/Release?
Suppose we add a method to a lock, to ask if the lock is free. it returns true. Is the lock:
Free? Busy?
Don’t know?
Suppose
Too Much Milk, #4
Locks allow concurrent code to be much simpler:
lock.acquire(); if (!milk)
buy milk lock.release();
Lock Example: Malloc/Free
char *malloc (n) { heaplock.acquire(); p = allocate memory heaplock.release(); return p;
}
void free(char *p) { heaplock.acquire(); put p back on free list heaplock.release();
}
Rules for Using Locks
Double Checked Locking
if (p == NULL) {
lock.acquire(); if (p == NULL) {
p = newP();
}
lock.release();
}
use p->field1
newP() {
tmp = malloc(sizeof(p)); tmp->field1 = …
tmp->field2 = … return tmp;
}
Single Checked Locking
lock.acquire();
if (p == NULL) {
p = newP();
}
lock.release(); use p->field1
newP() {
tmp = malloc(sizeof(p)); tmp->field1 = …
tmp->field2 = … return tmp;
}
Example: Bounded Buffer
tryget() { lock.acquire(); item = NULL;
if (front < tail) {
item = buf[front % MAX]; front++;
}
lock.release(); return item;
}
tryput(item) { lock.acquire(); success = FALSE;
if ((tail – front) < MAX) {
buf[tail % MAX] = item; tail++;
success = TRUE;
}
lock.release(); return success;
}
Initially: front = tail = 0; lock = FREE; MAX is buffer capacity
Question