
9/22/20 CS 422/522 Design & Implementation of Operating Systems Lectures 8-9: Implementing Synchronization Zhong Shao Dept. of Computer Science Yale University 1 The big picture Concurrent Applications Shared Objects Bounded Bu!er Barrier Synchronization Variables Semaphores Locks Condition Variables Atomic Instructions Interrupt Disable Test-and-Set Hardware Multiple Processors Hardware Interrupts 2 1 9/22/20 Semaphores (Dijkstra 1965) ! Semaphores are a kind of generalized lock. * They are the main synchronization primitives used in the earlier Unix. ! Semaphores have a non-negative integer value, and support two operations: – semaphore->P(): an atomic operation that waits for semaphore to become positive, then decrements it by 1 – semaphore->V(): an atomic operation that increments semaphore by 1, waking up a waiting P, if any. ! Semaphores are like integers except: (1) none-negative values; (2) only allow P&V --- can’t read/write value except to set it initially; (3) operations must be atomic: two P’s that occur together can’t decrement the value below zero. Similarly, thread going to sleep in P won’t miss wakeup from V, even if they both happen at about the same time. 3 Implementing semaphores P means “test” (proberen in Dutch) V means “increment” (verhogen in Dutch) class Semaphore { int value = initialValue; } Semaphore::V() { Semaphore::P() { Disable interrupts; Disable interrupts; if anyone on wait queue { while (value == 0) { Take a waiting thread off wait Put on queue of threads waiting queue and put it on the ready for this semaphore; queue; Go to sleep; } } value = value + 1; value = value - 1; Enable interrupts Enable interrupts } } 4 2 9/22/20 Binary semaphores Like a lock; also known as “mutex”; can only have value 0 or 1 (unlike the previous “counting semaphore” which can be any non-negative integers) class Semaphore { int value = 0 or 1; } Semaphore::V() { Semaphore::P() { Disable interrupts; Disable interrupts; if anyone on wait queue { while (value == 0) { Take a waiting thread off wait Put on queue of threads waiting queue and put it on the ready for this semaphore; queue; Go to sleep; } } value = 1; value = 0; Enable interrupts Enable interrupts } } 5 How to use semaphores ! Binary semaphores can be used for mutual exclusion: initial value of 1; P() is called before the critical section; and V() is called after the critical section. semaphore->P(); // critical section goes here semaphore->V(); ! Scheduling constraints – having one thread to wait for something to happen * Example: Thread::Join, which must wait for a thread to terminate. By setting the initial value to 0 instead of 1, we can implement waiting on a semaphore ! Controlling access to a finite resource 6 3 9/22/20 Scheduling constaints ! Something must happen after one another Initial value of semaphore = 0; Fork a child thread Thread::Join calls P // will wait until something // makes the semaphore positive ------------------------------------------------------------------------- Thread finish calls V // makes the semaphore positive // and wakes up the thread // waiting in Join 7 Scheduling with semaphores ! In general, scheduling dependencies between threads T1, T2, …, Tn can be enforced with n-1 semaphores, S1, S2, …, Sn-1 used as follows: – T1 runs and signals V(S1) when done. – Tm waits on Sm-1 (using P) and signals V(Sm) when done. ! (contrived) example: schedule print(f(x,y)) float x, y, z; sem Sx = 0, Sy = 0, Sz = 0; T1: T2: T3: x = …; P(Sx); P(Sz); V(Sx); P(Sy); print(z); y = …; z = f(x,y); … V(Sy); V(Sz); … ... 8 4 9/22/20 Producer-consumer with semaphores (1) ! Correctness constraints * consumer must wait for producer to fill buffers, if all empty (scheduling constraints) * producer must wait for consumer to empty buffers, if all full (scheduling constaints) * Only one thread can manipulate buffer queue at a time (mutual exclusion) ! General rule of thumb: use a separate semaphore for each constraint Semaphore fullBuffers; // consumer’s constraint // if 0, no coke in machine Semaphore emptyBuffers; // producer’s constraint // if 0, nowhere to put more coke Semaphore mutex; // mutual exclusion 9 Producer-consumer with semaphores (2) Semaphore fullBuffers = 0; // initially no coke Semaphore emptyBuffers = numBuffers; // initially, # of empty slots semaphore used to Consumer() { // count how many resources there are fullBuffers.P(); // check if there is Semaphore mutex = 1; // no one using the machine // a coke in the machine mutex.P(); // make sure no one Producer() { // else is using machine emptyBuffers.P(); // check if there is space // for more coke take 1 Coke out; mutex.P(); // make sure no one else // is using machine mutex.V(); // next person’s turn emptyBuffers.V(); // tell producer put 1 Coke in machine; // we need more } mutex.V(); // ok for others to use machine fullBuffers.V(); // tell consumers there is now // a Coke in the machine What if we have 2 producers and } 2 consumers? 10 5 9/22/20 Order of P&Vs --- what can go wrong Semaphore fullBuffers = 0; // initially no coke Semaphore emptyBuffers = numBuffers; // initially, # of empty slots semaphore used to Consumer() { // count how many resources there are mutex.P(); // make sure no one Semaphore mutex = 1; // no one using the machine // else is using machine fullBuffers.P(); // check if there is Producer() { // a coke in the machine mutex.P(); // make sure no one else // is using machine take 1 Coke out; emptyBuffers.P(); // check if there is space // for more coke emptyBuffers.V(); // tell producer // we need more put 1 Coke in machine; mutex.V(); // next person’s turn } fullBuffers.V(); // tell consumers there is now // a Coke in the machine Deadlock---two or more processes are mutex.V(); // ok for others to use machine waiting indefinitely for an event that } can be caused by only one of the waiting processes. 11 Implementing synchronization Concurrent Applications Shared Objects Bounded Bu!er Barrier Synchronization Variables Semaphores Locks Condition Variables Atomic Instructions Interrupt Disable Test-and-Set Hardware Multiple Processors Hardware Interrupts 12 6 9/22/20 Implementing synchronization Take 1: using memory load/store – See too much milk solution/Peterson’s algorithm Take 2: Lock::acquire() { disable interrupts } Lock::release() { enable interrupts } Take 3: queueing locks No point on running the threads waiting for locks 13 Lock implementation, uniprocessor Lock::acquire() { class Lock { private int value = FREE; disableInterrupts(); private Queue waiting; public void acquire(); public void release(); if (value == BUSY) { } waiting.add(myTCB); Lock::release() { myTCB->state = WAITING; disableInterrupts(); next = readyList.remove(); if (!waiting.Empty()) { switch(myTCB, next); next = waiting.remove(); myTCB->state = RUNNING; next->state = READY; } else { value = BUSY; readyList.add(next); } } else { value = FREE; enableInterrupts(); } } enableInterrupts(); } 14 7 9/22/20 Multiprocessor ! Read-modify-write instructions – Atomically read a value from memory, operate on it, and then write it back to memory – Intervening instructions prevented in hardware ! Examples – Test and set – Intel: xchgb, lock prefix – Compare and swap ! Any of these can be used for implementing locks and condition variables! 15 Spinlocks A spinlock is a lock where the processor waits in a loop for the lock to become free – Assumes lock will be held for a short time – Used to protect the CPU scheduler and to implement locks Spinlock::acquire() { while (testAndSet(&lockValue) == BUSY) ; } Spinlock::release() { lockValue = FREE; memorybarrier(); } 16 8 9/22/20 How many spinlocks? ! Various data structures – Queue of waiting threads on lock X – Queue of waiting threads on lock Y – List of threads ready to run ! One spinlock per kernel? – Bottleneck! ! Instead: – One spinlock per lock – One spinlock for the scheduler ready list * Per-core ready list: one spinlock per core 17 What thread is currently running? ! Thread scheduler needs to find the TCB of the currently running thread – To suspend and switch to a new thread – To check if the current thread holds a lock before acquiring or releasing it ! On a uniprocessor, easy: just use a global ! On a multiprocessor, various methods: – Compiler dedicates a register (e.g., r31 points to TCB running on the this CPU; each CPU has its own r31) – If hardware has a special per-processor register, use it – Fixed-size stacks: put a pointer to the TCB at the bottom of its stack * Find it by masking the current stack pointer 18 9 9/22/20 Lock implementation, multiprocessor class Lock { Lock::release() { private int value = FREE; private SpinLock spinLock; private Queue waiting; …} disableInterrupts(); Lock::acquire() { spinLock.acquire(); disableInterrupts(); if (!waiting.Empty()) { spinLock.acquire(); next = waiting.remove(); if (value == BUSY) { scheduler->makeReady(next); waiting.add(myTCB); } else { scheduler->suspend(&spinlock); value = FREE; } else { } value = BUSY; spinLock.release(); spinLock.release(); enableInterrupts(); } } enableInterrupts(); } 19 Lock implementation, multiprocessor (cont’d) class Scheduler { void private: Scheduler::suspend(SpinLock *lock) { Queue readyList; TCB *chosenTCB; SpinLock schedulerSpinLock; public: void suspend(SpinLock *lock); disableInterrupts(); void makeReady(Thread *thread); schedulerSpinLock.acquire(); } lock->release(); void Scheduler::makeReady(TCB *thread) { runningThread->state = WAITING; disableInterrupts(); chosenTCB = readyList.getNextThread(); schedulerSpinLock.acquire(); thread_switch(runningThread,
Details
-
File Typepdf
-
Upload Time-
-
Content LanguagesEnglish
-
Upload UserAnonymous/Not logged-in
-
File Pages17 Page
-
File Size-