370 likes | 380 Views
Learn about implementing threads and locks on a uni-processor, including thread interactions, shared data access, use of locks, monitors, and semaphores, and the different states of threads.
E N D
CPS110: Implementing threads/locks on a uni-processor Landon Cox
Recall, thread interactions • Threads can access shared data • Use locks, monitors, and semaphores • What we’ve done so far • Threads also share hardware • CPU (uni-processor) • Memory
Private vs global thread state • What state is private to each thread? • Code (like lines of a play) • PC (where actor is in his/her script) • Stack, SP (actor’s mindset) • What state is shared? • Global variables, heap • (props on set)
Thread control block (TCB) • Running thread’s private data is on CPU • Non-running thread’s private state is in TCB • Thread control block (TCB) • Container for non-running thread’s private data • (PC, code, SP, stack, registers)
Thread control block Address Space TCB1 PC SP registers TCB2 PC SP registers TCB3 PC SP registers Ready queue Code Code Code Stack Stack Stack Thread 1 running CPU PC SP registers
Thread control block Address Space TCB2 PC SP registers TCB3 PC SP registers Ready queue Code Stack Stack Stack Thread 1 running CPU PC SP registers
Thread states • Running • Currently using the CPU • Ready • Ready to run, but waiting for the CPU • Blocked • Stuck in lock (), wait () or down ()
Switching threads • What needs to happen to switch threads? • Thread returns control to OS • For example, via the “yield” call • OS chooses next thread to run • OS saves state of current thread • To its thread control block • OS loads context of next thread • From its thread control block • Run the next thread Project 1: swapcontext
1. Thread returns control to OS • How does the thread system get control? • Could wait for thread to give up control • (internal events) • Thread might block inside lock or wait • Thread might call into kernel for service • (system call) • Thread might call yield • Are internal events enough?
1. Thread returns control to OS • External events • (events not initiated by the thread) • Hardware interrupts • Transfer control directly to OS interrupt handlers • From 104 • CPU checks for interrupts while executing • Jumps to OS code with interrupt mask set • Interrupts lead to pre-emption (a forced yield) • Common interrupt: timer interrupt
2. Choosing the next thread • If no ready threads, just spin • Modern CPUs: execute a “halt” instruction • Project 1: exit if no ready threads • Loop switches to thread if one is ready • Many ways to prioritize ready threads • Will discuss a little later in the semester
3. Saving state of current thread • What needs to be saved? • Registers, PC, SP • What makes this tricky? • Need registers to save state • But you’re trying to save all the registers • Saving the PC
4. OS loads the next thread • Where is the thread’s state/context? • Thread control block (in memory) • How to load the registers? • Use load instructions to grab from memory • How to load the stack? • Stack is already in memory, load SP
5. OS runs the next thread • How to resume thread’s execution? • Jump to the saved PC • On whose stack are these steps running? or Who is running these steps? • The thread that called yield • (or was interrupted or called lock/wait) • How does this thread run again? • Some other thread must switch to it
Example thread switching Thread 1 print “start thread 1” yield () print “end thread 1” Thread 2 print “start thread 2” yield () print end thread 2” yield () print “start yield (thread %d)” switch to next thread (swapcontext) print “end yield (thread %d)” Thread 1 output Thread 2 output -------------------------------------------- start thread 1 start yield (thread 1) start thread 2 start yield (thread 2) end yield (thread 1) end thread 1 end yield (thread 2) end thread 2 Note: this assumes no pre-emptions. Programmers must assume pre-emptions, so this output is not guaranteed.
Creating a new thread • Also called “forking” a thread • Idea: create initial state, put on ready queue • Allocate, initialize a new TCB • Allocate a new stack • Make it look like thread was going to call a function • PC points to first instruction in function • SP points to new stack • Stack contains arguments passed to function • Project 1: use makecontext • Add thread to ready queue
Using interrupt disable-enable • Disable-enable on a uni-processor • Assume atomic (can use atomic load/store) • How do threads get switched out? • Internal events (yield, I/O request) • External events (interrupts, e.g. timers) • Easy to prevent internal events • Use disable-enable to prevent external events
Why use locks? disable interrupts while (1){} • If we have disable-enable, why do we need locks? • Program could bracket critical sections with disable-enable • Might not be able to give control back to thread library • Can’t have multiple locks (over-constrains concurrency) • Project 1: only disable interrupts in thread library
Lock implementation #1 unlock () { disable interrupts value = FREE enable interrupts } lock () { disable interrupts while (value != FREE) { enable interrupts disable interrupts } value = BUSY enable interrupts } Why is it ok for lock code to disable interrupts? It’s in the trusted kernel (we have to trust something). Disable interrupts, busy-waiting
Lock implementation #1 unlock () { disable interrupts value = FREE enable interrupts } lock () { disable interrupts while (value != FREE) { enable interrupts disable interrupts } value = BUSY enable interrupts } Do we need to disable interrupts in unlock? Only if “value = FREE” is multiple instructions (safer) Disable interrupts, busy-waiting
Lock implementation #1 unlock () { disable interrupts value = FREE enable interrupts } lock () { disable interrupts while (value != FREE) { enable interrupts disable interrupts } value = BUSY enable interrupts } Why enable-disable in lock loop body? Otherwise, no one else will run (including unlockers) Disable interrupts, busy-waiting
Using read-modify-write instructions • Disabling interrupts • Ok for uni-processor, breaks on multi-processor • Why? • Could use atomic load-store to make a lock • Inefficient, lots of busy-waiting • Hardware people to the rescue!
Lock implementation #2 unlock () { value = 0 } lock () { while (test&set(value) == 1) { } } What happens if value = 1? What happens if value = 0? Test&set, busy-waiting Initially, value = 0
Locks and busy-waiting • All implementations have used busy-waiting • Wastes CPU cycles • To reduce busy-waiting, integrate • Lock implementation • Thread dispatcher data structures
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } Interrupt disable, no busy-waiting
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } This is called a “hand-off” lock. unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } Who gets the lock after someone calls unlock?
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } This is called a “hand-off” lock. unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } Who might get the lock if it weren’t handed-off directly? (e.g. if value weren’t set BUSY in unlock)
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } This is called a “hand-off” lock. unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } What does this mean? Are we saving the PC?
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } This is called a “hand-off” lock. unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } Why a separate queue for the lock?
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } This is called a “hand-off” lock. unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } Project 1 note: you must guarantee FIFO ordering of lock acquisition.
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { enable interrupts add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } When and how could this fail?
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock enable interrupts switch to next ready thread // don’t add to ready queue } enable interrupts } unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } When and how could this fail?
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } Putting lock thread on lock wait queue, switch must be atomic. Must call switch with interrupts off.
How is switch returned to? • Review from last time • Think of switch as three phases • Save current location (SP, PC) • Point processor to another stack (SP’) • Jump to another instruction (PC’) • Only way to get back to a switch • Have another thread call switch
Lock implementation #3 lock () { disable interrupts if (value == FREE) { value = BUSY // lock acquire } else { add thread to queue of threads waiting for lock switch to next ready thread // don’t add to ready queue } enable interrupts } unlock () { disable interrupts value = FREE if anyone on queue of threads waiting for lock { take waiting thread off queue, put on ready queue value = BUSY } enable interrupts } What is lock() assuming about the state of interrupts after switch returns?
Interrupts and returning to switch • Lock() can assume that switch • Is always called with interrupts disabled • On return from switch • Previous thread must have disabled interrupts • Next thread to run • Becomes responsible for re-enabling interrupts • Invariants: threads promise to • Disable interrupts before switch is called • Re-enable interrupts after returning from switch
Thread A Thread B yield () { disable interrupts … switch enable interrupts } // exit thread library function <user code> lock () { disable interrupts … switch back from switch … enable interrupts } // exit yield <user code> unlock () // moves A to ready queue yield () { disable interrupts … switch back from switch … enable interrupts } // exit lock <user code>