760 likes | 859 Views
Process/Thread Synchronization. Modified from Chapter 6 for 170 with Nachos threads, Tao Yang, 2012. Topics. The Critical-Section Problem & Solutions. Locks Semaphores Condition variables (monitors). Implementation Software solutions: Peterson’s Solution Synchronization Hardware
E N D
Process/Thread Synchronization Modified from Chapter 6 for 170 with Nachos threads, Tao Yang, 2012
Topics • The Critical-Section Problem & Solutions. • Locks • Semaphores • Condition variables (monitors). • Implementation • Software solutions: Peterson’s Solution • Synchronization Hardware • Synchronization Examples
Objectives • To introduce the critical-section problem, whose solutions can be used to ensure the consistency of shared data • To present both software and hardware solutions of the critical-section problem • To introduce the concept of an atomic transaction and describe mechanisms to ensure atomicity
A. Frank - P. Weisberg Multiple Producers and Consumers
Background • Maintain countthat keeps track of the number of items to consume in the buffer.. • Concurrent access to shared data may result in data inconsistency
Producer Code while (true) { while (count == BUFFER_SIZE) ; // do nothing buffer [in] = nextProduced; in = (in + 1) % BUFFER_SIZE; count++; }
Consumer while (true) { while (count == 0) ; // do nothing nextConsumed = buffer[out]; out = (out + 1) % BUFFER_SIZE; count--; }
Definitions • Race Conditions: two or more processes are reading and writing on shared data and the final result depends on who runs precisely when • Mutual exclusion: making sure that if one process is accessing a shared memory, the other will be excluded from doing the same thing • Critical region: the part of the program where shared variables are accessed
Race Conditions: Example Count=5 Consumer thread Producer thread Count++ Count-- Is count still 5?
Race Conditions: Example Count=5 Consumer thread Producer thread Count++: register1 = count register1 = register1 + 1 count = register1 Count—: register2 = count register2 = register2 - 1 count = register2 Is count still 5?
Race Conditions: Example Count=5 Consumer thread Producer thread Count++: register1 = count register1 = register1 + 1 count = register1 Count—: register2 = count register2 = register2 - 1 count = register2 Is count still 5?
Race Condition • “count = 5” initially: S0: producer execute register1 = count {register1 = 5}S1: producer execute register1 = register1 + 1 {register1 = 6} S2: consumer execute register2 = count {register2 = 5} S3: consumer execute register2 = register2 - 1 {register2 = 4} S4: producer execute count = register1 {count = 6 } S5: consumer execute count = register2 {count = 4}
Property of Critical-Section Solutions • Mutual Exclusion – Only one can enter the critical section. • Progress- If some processes wish to enter their critical section and nobody is in the critical section, then one of them will enter in a limited time. • Bounded Waiting - If one process starts to wait for entering an critical section, there is a limit on the number of times other processes entering the section before this process enters.
Solution to Critical-section Problem Locks Semaphore Conditional Variables acquire lock critical section release lock remainder section
Semaphore • Semaphore S – integer variable • Can only be accessed /modified via two indivisible (atomic) operations • wait (S) { //also called P() while S <= 0 ; // wait S--; } • signal (S) { //also called V() S++; }
Semaphore as General Synchronization Tool • Countingsemaphore – initial value representing how many threads can be in the critical section. • Binary semaphore (also known as mutex locks) – integer value ranged between 0 and 1. • Provides mutual exclusion Semaphore mutex; // initialized to 1 do { wait (mutex); // Critical Section signal (mutex); } while (TRUE);
Nachos Threads • class Thread{ public: Thread (char *name); ~Thread(); void Fork( void (*Func)( int), int arg); void Yield(); void Finish(); }
Nachos Threads: Methods supported • Thread(char *debugName). Create a thread. • Fork(VoidFunctionPtr func, int arg). A thread starts to execute a function. • Yield(). Suspend the calling thread and the system selects a new one for execution. • Sleep(). Suspend the current thread, change its state to BLOCKED, and remove it from the ready list • Finish()
Sample Example of Nacho Threads main () { Thread *t1 = new Thread("forked thread1"); Thread *t2 = new Thread("forked thread2"); t1->Fork(SimpleThread, 1); t2->Fork(SimpleThread, 2); SimpleThread(3); } SimpleThread(int i) { printf(“Hello %d\n”, i); currentThread->Yield(); } Create 2 new threads. Start to fork and execute a function in each child thread. Parent also executes the same function Function executed by threads
Nachos Lock Interface • Lock can be in one of two states: locked or unlocked • class Lock { public: Lock (char *name); //create a lock with unlocked state ~Lock(); void Acquire(); //Atomically waits until the lock is unlocked, and then sets the lock to be locked. void Release(); //Atomically changes the state to be unlocked. Only the thread who owns the lock can release. }
How to use locks • Typically associate a lock with a piece of shared data for mutual execlusion. • When a thread needs to access, it first acquires the lock, and then accesses data. Once access completes, it releases the lock. • Example Lock *k= new Lock (“Lock”); k->Acquire(); Perform critical section operations. k->Release(); }
Thread synchronization: Example The desired value for a is 2 at the end. Possible output: T0: a=1 T1: a=2 T0:a=2 T1: a=1 T0: a=1 T1:a=1 int a=0; void sum (int p){ a=a+1; printf(“T%d : a= %d\n”, p,a); } void main() { Thread *t=new Thread (“Child”); t->Fork(sum,1); sum(0); }
Synchronization with Locks: Example int a=0; Lock *k; void sum (int p){ int c; k->Acquire(); a=a+1; c=a; k->Release() printf(“%d : a= %d\n”, p,c); } void main() { Thread *t=new Thread (“Child”); k=new Lock(“Lock”); t->Fork(sum,1); sum(0); }
Semaphores in Nachos • class Semaphore{ public: Semaphore (char *name, int counter); // initial counter value ~Semaphore(); void P(); //Atomically waits until the counter is greater than 0 and then decreases the counter void V(); //Atomically increases the counter }
Synchronization with Semaphore: Example int a=0; Semaphore *s; void sum (int p){ int c; s->P(); a=a+1; c=a; s->V(); printf(“%d : a= %d\n”, p,c); } void main() { Thread *t=new Thread (“Child”); s=new Semaphore(“S”,1); t->Fork(sum,1); sum(0); }
Semaphore for consumer-producer problem with unbounded buffer • Semaphore can do more than mutual exclusion, and can synchronize a consumer/producer pipe • Semaphore *data =new Semaphore(“Data”, 0); • Indicate # of data items available for consuming • Consumer thread: while(1) { data-> P(); //wait until an item is available consume next item; } • Producer thread: while(1){ produce next item; data->V(); //notify an item is available }
consumer-producer problem with bounded buffer • Two semaphores: • Semaphore *data =new Semaphore(“Data”, 0); • Indicate # of data items available for consuming • Semaphore *space =new Semaphore(“Space”, 10); • Indicate # of 10 data slots available. • Producer thread: while(1){ produce next item; data->V(); //notify item is available }
consumer-producer problem with bounded buffer • Two semaphores: • Semaphore *data =new Semaphore(“Data”, 0); • Indicate # of data items available for consuming • Semaphore *space =new Semaphore(“Space”, 10); • Indicate # of 10 data slots available. • Producer thread: while(1){ space->P();//wait until space is available produce next item; data->V(); //notify data is available }
consumer-producer problem with bounded buffer • Semaphore *data =new Semaphore(“Data”, 0); • Semaphore *space =new Semaphore(“Space”, 10); • Producer thread: while(1){ space->P();//wait until space is available produce next item; data->V(); //notify data is available } • Consumer thread: while(1){ data->P();//wait until data is available consume next item; }
consumer-producer problem with bounded buffer • Semaphore *data =new Semaphore(“Data”, 0); • Semaphore *space =new Semaphore(“Space”, 10); • Producer thread: while(1){ space->P();//wait until space is available produce next item; data->V(); //notify data is available } • Consumer thread: while(1){ data->P();//wait until data is available consume next item; space->V();//notify space is available } }
Solution to Critical-section Problem Locks Semaphore Conditional Variables
Condition Variables • Improvement on busy waiting for acquiring a lock (spin locks) • A thread is looping forever in checking if a lock is available or not. • The process “spins” Waste CPU resource. • Condition variables allow the waiting thread to be blocked, placed in a waiting queue, yielding CPU resource to somebody else. • Similar problem in semaphore implementation • wait (S) { //also called P() while (S <= 0) ; // no-op S--; }
Nachos Condition Variables • Similar to Pthreads Condition Variables • class Condition { public: Condition(char *name); ~Condition(); void Wait(Lock *ConditionLock); //Atomically releases the lock and waits. When it is returned, the lock is reacquired again. void Signal(Lock *ConditionLock); //Wake up one waiting thread to run. The lock is not released. void Broadcast(Lock *ConditionLock); //Wake up all threads waiting on the condition. The lock is not released. }
How to Use Condition Variables • Associate a lock (L) and condition variable (cv) with data items to be accessed. • An abstraction with lock/condition is known as monitor (Chapter 6.7). • Typical program flow: • Thread 1 L.Acquire(); When condition is not satisfied, cv.Wait(L); Critical Section; L.Release(); • Thread 2: L.Acquire(); When condition can satisfy, cv.Signal(L). L.Relase();
Condition Variables for consumer-producer problem with unbounded buffer • int avail=0; // # of data items available for consumption • Lock *L; Condition *cv= new Condition (“condition”); • Consumer thread: L.Acquire(); while (avail <=0) cv.Wait(L); consume next item; avail = avail-1; L.Release(); • Producer thread: L.Acquire(); Produce next item; availl = avail+1; cv.Signal(L); //notify an item is available L.Release();
When to use condition broadcast() • When waking up one thread to run is not sufficient. • Example: concurrent malloc()/free() for allocation and deallocation of objects with non-uniform sizes. • Lock *L; Condition *cv= new Condition (“condition”); • char *malloc(int s) { L.Acquire(); while( cannot allocate a chunk of size s) cv.Wait(L); allocate free space of size s; L.Release(); return the chunk allocated. }
Use Broadcast() when space is freed • void free (char *m){ L.Acquire(); deallocate m; cv.Broadcast(L); //wake up all threads waiting for space L.Release(); }
Running trace of malloc()/free() • Initially 10 bytes are free. • m() stands for malloc(). f() for free() Thread 2: m(5) – wait Resume m(5)-succ f(5) –broadcast Thread 3: m(5) – wait Resume m(5)-succ m(3) –wait Resume m(3)-succ Thread 1: m(10) – succ f(10) –broadcast m(7) – wait Resume m(7)-wait
Implementation of Synchronization Primitives • Software solutions through shared memory data checking or message passing. • Hardware-assisted solutions • Properties (e.g. for implementing a lock) • Mutual exclusion: only one gets a lock. • Progress: when multiple threads try to acquire a lock, one can get it if nobody has it. • Bounded waiting: lock waiting time is limited.
Software Solution for 2 processes/threads • The two processes share one variable: • intturn; // whose turn for the critical section • Process 0 • Process 1: • Mutual exclusion? Progress? Bounded waiting? while (turn != 0); critical section turn= 1; //give turn to P1 while (turn != 1); critical section turn= 0; //give turn to P0
Mutual exclusion? • Prove by contraction. Assume both Processes 0 and 1 are in the critical section. • Process 0 • Process 1: • (turn==0) and (turn==1) is false while (turn != 0); critical section turn= 1; //give turn to P1 turn==0 while (turn != 1); critical section turn= 0; //give turn to P0 turn==1
Peterson’s Solution • Two process software solution (Text book chapter 6.3) • Assume that the LOAD and STORE instructions are atomic; that is, cannot be interrupted. • The two processes share two variables: • intturn; • indicates whose turn it is to enter the critical section. • Boolean ready[2] • indicate if a process is ready to enter the critical section. ready[0]= true implies that process P0 is ready!
Peterson’s Algorithm Process P0: ready[0] = TRUE; turn = 1; while (ready[1] && turn == 1); critical section ready[0] = FALSE; Enter critical section Exit critical section Process P1: ready[1] = TRUE; turn = 0; while (ready[0] && turn == 0); critical section ready[1] = FALSE; Mutual exclusion? Progress? Bounded waiting?
Mutual Execution? Prove by contradition Process P0: ready[0] = TRUE; turn = 1; while (ready[1] && turn == 1); critical section ready[0] = FALSE; ready[0]=T and (Ready[1]=F or turn=0) Process P1: ready[1] = TRUE; turn = 0; while (ready[0] && turn == 0); critical section ready[1] = FALSE; ready[1]=T and (Ready[0]=F or turn=1) Both conditions are true, which is not possible
Progress? Can both P0/P1 wait forever? Process P0: ready[0] = TRUE; turn = 1; while (ready[1] && turn == 1); critical section ready[0] = FALSE; (Ready[1]=T and turn=1) Process P1: ready[1] = TRUE; turn = 0; while (ready[0] && turn == 0); critical section ready[1] = FALSE; (Ready[0]=T and turn=0) Both conditions are true, which is not possible
Progress? Can P0 wait forever after P1 leaves Process P0: ready[0] = TRUE; turn = 1; while (ready[1] && turn == 1); critical section ready[0] = FALSE; (ready[1]=T and turn=1) Process P1: ready[1] = TRUE; turn = 0; while (ready[0] && turn == 0); critical section ready[1] = FALSE; ready[1]=F Both conditions are true, which is not possible.
Bounded waiting? Can P0 wait forever after P1 leaves Process P0: ready[0] = TRUE; turn = 1; while (ready[1] && turn == 1); critical section ready[0] = FALSE; (ready[1]=T and turn=1) Process P1: ready[1] = TRUE; turn = 0; while (ready[0] && turn == 0); critical section ready[1] = FALSE; ready[1]=F Both conditions are true, which is not possible. P0 gets in after at most one entry by P1
Hardware Solution for Synchronization • Many systems provide hardware support for critical section code • Uniprocessors – could disable interrupts • Currently running code would execute without preemption • Project 1. • Generally too inefficient on multiprocessor systems • Operating systems using this not broadly scalable • Modern machines provide special atomic hardware instructions • Atomic = non-interruptable • Either test memory word and set value • Or swap contents of two memory words
TestAndSet Instruction • Definition: Get the value of a shared variable and then set it to TRUE atomically. boolean TestAndSet (boolean *target) { boolean rv = *target; *target = TRUE; return rv: }