View
213
Download
0
Tags:
Embed Size (px)
Citation preview
1
Synchronization!
Welcome to the most important part of this course!Welcome to the most important part of this course!
OperatingSystems
Paulo MarquesDepartamento de Eng. InformáticaUniversidade de [email protected]
2006
/200
7
4. Synchronization4.1 A Systems Programmer’s View
3
Disclaimer
This slides and notes are heavily based on the companion material of [Silberschatz05].The original material can be found at: http://codex.cs.yale.edu/avi/os-book/os7/slide-dir/index.html
In some cases, material from [Stallings04] may also be used. The original material can be found at: http://williamstallings.com/OS/OS5e.html
The respective copyrights belong to their owners.
4
Consider the following library call
Process A
send_to_printer(59, “What a beautiful day!”);
Process B
send_to_printer(12, “I hate going to school!”);
5
Resulting in…
Race condition: The situation where several processes access – and manipulate shared data concurrently. The result critically depends on timing of these processes, which are “racing”.
Job from user --- 59 ---What a beautiful day!-------------------------------Job from user --- 12 ---I hate going to school!-------------------------------
What I expected!
Job from user --- 59 ---Job from user --- 12 ---What I hate a beautiful day!-------------------------------going to school!-------------------------------
Or not so!!!!
6
Mutual Exclusion!
This routine is neither Thread-Safe nor Reentrant!
This piece of code has to be executed Atomically,in Mutual Exclusion! These three linesconstitute a Critical Section.
7
Mutexes
A mutex is a synchronization primitive available for processes and threads. It has two primitives: lock(): allows a thread to acquire the mutex, ensuring that
only one flow of execution exists inside the critical section. If a second thread calls lock(), it becomes blocked.
unlock(): signals that a thread is leaving the critical section. If there are other threads waiting for the critical section, one of them is allowed to run: it becomes ready.
Only one thread/process can be in here!
9
Semaphores – Powerful Friends
A semaphore is a synchronization object Controlled access to a counter (a value) Two operations are supported: wait() and post()
wait() If the semaphore is positive, decrement it and continue If not, block the calling thread (process)
post() Increment the semaphore value If there was any (process) thread blocked due to the
semaphore, unblock one of them.
Semaphores are used to count things! Blocked Processes in a semaphore do
not consume resources (CPU)!
Semaphores are used to count things! Blocked Processes in a semaphore do
not consume resources (CPU)!
11
“Real Semaphores”
System V Semaphores Works with semaphore arrays semget(), semctl(), semop() A little bit hard to use by themselves
Use a library to encapsulate them!
POSIX Semaphores Quite easy to use sem_init(), sem_close(), sem_post(), sem_wait() Also work with threads! But... in Linux, they only work with threads (kernel 2.4)
Java Typically uses “monitors”, now it has: java.util.concurrent.Semaphore
.NET Typically uses “monitors”, but it has: System.Threading.Semaphore
13
Classical – Producer/Consumer Problem
A producer puts elements on a finite buffer. If the buffer is full, it blocks until there’s space.
The consumer retrieves elements. If the buffer is empty, it blocks until something comes along.
We will need three semaphores One to count the empty slots One to count the full slots One to provide for mutual exclusion to the shared buffer
Producer Consumer
14
Producer/Consumer – Basic Implementation
Producer Consumer
empty full
read_pos write_pos
put_element(e) { sem_wait(empty); sem_wait(mutex); buf[write_pos] = e; write_pos = (write_pos+1) % N; sem_post(mutex); sem_post(full);}
mutex
get_element() { sem_wait(full); sem_wait(mutex); e = buf[read_pos]; read_pos = (read_pos+1) % N; sem_post(mutex); sem_post(empty); return e;}
17
Main loop
void producer() { for (int i=TOTAL_VALUES; i>0; i--) { printf("[PRODUCER] Writing %d\n", i); put_element(i); }}
void consumer() { for (int i=0; i<TOTAL_VALUES; i++) { int e = get_element(); printf("[CONSUMER] Retrieved %d\n", e); sleep(1); } terminate();}
void main(int argc, char* argv[]) { init();
if (fork() == 0) { producer(); exit(0); } else { consumer(); exit(0); }}
18
put_element() and get_element()
void put_element(int e) { sem_wait(sem, EMPTY); sem_wait(sem, MUTEX);
buf[write_pos] = e; write_pos = (write_pos+1) % N;
sem_post(sem, MUTEX); sem_post(sem, FULL);}
int get_element() { sem_wait(sem, FULL); sem_wait(sem, MUTEX);
int e = buf[read_pos]; read_pos = (read_pos+1) % N;
sem_post(sem, MUTEX); sem_post(sem, EMPTY);
return e;}
19
init() and terminate()
int sem, shmid;int write_pos, read_pos;int* buf;
void init() { sem = sem_get(3, 0); sem_setvalue(sem, EMPTY, N); // N is the number of slots sem_setvalue(sem, FULL, 0); sem_setvalue(sem, MUTEX, 1);
write_pos = read_pos = 0;
shmid = shmget(IPC_PRIVATE, N*sizeof(int), IPC_CREAT|0700); buf = (int*) shmat(shmid, NULL, 0);}
void terminate() { sem_close(sem); shmctl(shmid, IPC_RMID, NULL);}
20
What should I care?
When you read some data from a pipe…
When you write some data to the network…
When your windows application is processing all those “Open Window”, “Close Window” messages…
When your bank is processing the money you are depositing…
When the a space probe sends images and telemetry from space…
… When you are playing Unreal and all those guys simply “must die”!
21
Classical – Readers/Writers Problem
Writer processes have to update shared data. Reader processes have to check the values of the
data. The should all be able to read at the same time.
Writer
Reader
Shared Data
Writer
Reader
Reader
Why is this different from the Producer/Consumer problem? Why not use a simple mutex?
Why is this different from the Producer/Consumer problem? Why not use a simple mutex?
22
Readers/Writers Problem
We will need two semaphores: One to stop the writers and guarantying mutual exclusion
when a writer is updating the data One to protect mutual exclusion of a shared variable that
counts readers
mutex
n_readers
stop_writers
Writer
Writer
Reader
Reader
Reader
23
Readers/Writers Algorithm (Priority to Readers)
mutex
n_readers
stop_writers
Writer
Writer
Reader Reader
write(e) { sem_wait(stop_writers); buffer = e; sem_post(stop_writers);}
read() { sem_wait(mutex); ++n_readers; if (n_readers == 1) sem_wait(stop_writers); sem_post(mutex); e = buffer;
sem_wait(mutex); --n_readers; if (n_readers == 0) sem_post(stop_writers); sem_post(mutex); return e;}
24
Some Considerations
The previous algorithm gives priority to readers Not always what you want to do
There’s a different version that gives priority to writers
Why should I care? This algorithm is the fundament of all database systems!
Concurrent reads of data; single update.
… One bank agency deposits some money in an account; at the same time, all over the country, many agencies can be reading it
… You are booking a flight. Although someone in England in also booking a flight, you and thousands of people can still see what are the available places in the place.
25
Classical – Buffer Cleaner Problem
A buffer can hold a maximum of N elements. When it is full, it should be immediately emptied. While the buffer is being emptied, no thread can put things into it.
Producer
CleanerProducer
Producer
27
Classical – Sleeping Barber
A barbershop has N waiting chairs and a barber chair
If there are no customers, the barber goes to sleep
When a customer enters: If all chairs are occupied, he
leaves If the barber is busy but
there are chairs, then the customer sits and waits
If the barber is sleeping, the customer wakes him up
Can you solve it??
28
Synchronization – Some basic rules...
Never Interlock waits! Locks should always be taken in the same order in
all processes Locks should be released in the reverse order they
have been taken
sem_wait(A)sem_wait(B)
// Critical Section
sem_post(B)sem_post(A)
sem_wait(B)sem_wait(A)
// Critical Section
sem_post(A)sem_post(B)
Deadlock!
One way to assure that you always take locks in the same order is to create a lock hierarchy. I.e. associate a number to each lock using a table and always lock in increasing order using that table as reference (index).
29
Synchronization – Some basic rules... (2)
Sometimes it is not possible to know what order to take when locking (or using semaphores) Example: you are using
two resources owned by the operating system. They are controlled by locks. You cannot be sure if another application is not using exactly the same resources and locking in reverse order.
In that case, use pthread_mutex_trylock() or sem_trywait() and back off if you are unsuccessful. Allow the system to make
progress and not deadlock!
30
Synchronization – Some basic rules... (3)
Mutexes are used for implementing mutual exclusion, not for signaling across threads!!! Only the thread that has locked a mutex can unlock it. Not doing
so will probably result in a core dump! To signal across threads use semaphores!
lock(&m)
unlock(&m)
lock(&m)
unlock(&m)
CORRECT!
lock(&m)
unlock(&m)
A B A B
(in mutual exclusion)
(in mutual exclusion)
(blocked)
INCORRECT!
31
Synchronization – Some basic rules... (4)
Don’t mess with thread priorities unless you really need to. Although apparently “correct” and “logical” in design, it’s quite
easy to make a system deadlock, live-lock or some threads to starve.
The Mars PathFinderwould run ok for a while and,after some time, it would stopand reset.
The watch-dog timer wasresetting the system because of some unknown reason…
32
The Mars PathFinder Problem
Priority Inversion Problem (in Mars Path Finder): Low priority thread locks a
semaphore. High priority thread starts to
execute and tries to lock the same semaphore. It’s suspended since it cannot violate the other thread’s lock.
Medium priority threads comes to execute and preempts the low priority thread. Since it doesn’t need the semaphore, it continues to execute.
Meanwhile the high priority thread is starving. After a while, the watchdog timer detects that the high priority thread is not executing and resets the machine.
A
B
C
starts toexecuteand getsthe lock
starts toexecute
tries to get the lock and is suspended
continuesto execute
is preempted asmedium prioritygets to execute
watchdog timer resets the machine as the high priorityone doesn’t get toexecute
(Solution: Thread Locking Priority Inheritance.)
33
Some important concepts to always remember!
DeadlockWhen two or more processes are unable to make progress being blocked waiting for each other
LivelockWhen two or more processes are alive and working but are unable to make progress
StarvationWhen a process is not being able to access resources that its needs to make progress
34
The inventor of the “Semaphore”
Edsger Dijkstra was one of the most brilliant computer scientists ever!
Inventor of the semaphore, one of the key contributions for modern operating systems; developed the THE operating system Used in all operating systems
today!
Created the Dijkstra algorithm for finding the shortest path in a graph Used in all computer networks
today (e.g. in OSPF routing)!
Wrote “A Case against the GO TO Statement”, and was one of the fathers of Algol-60 Which introduced the revolution of
structured programming!
Edsger Dijkstra
OperatingSystems
Paulo MarquesDepartamento de Eng. InformáticaUniversidade de [email protected]
2006
/200
7
4. Synchronization4.2. Other Synchronization Primitives
38
Semaphores are correct and very convenient but…Semaphores are correct and very convenient but…
EASY TO GET WRONG!EASY TO GET WRONG!
It’s easy to forget a wait() or post()… It’s easy to do wait() and post() in different semaphores
on opposite order It’s difficult to ensure correctness when several
semaphores are involved
39
Monitors
A monitor is an abstraction where only one thread or process can be executing at a time. Normally, it has associated data When inside a monitor, a thread executes in mutual
exclusion
Thread A
Monitor
Protected Data
Thread B
Thread C
Thread DWant to enterthe monitor…
In mutual exclusion
40
A Monitor in Java
Protected Data
Only one threadcan be inside thesemethods at a time;the others wait outside
41
Monitors (2)
Until now monitors look a lot like a simple MUTEX… Two more primitives are provided:
wait() Suspends the execution of the current thread, immediately relinquishing the monitor. The thread is put on a blocked threads list, waiting to be notified that “something” has changed.
notify() Informs one of the threads that is waiting for something to change that “something” has changed. Thus, one of the awaiting threads is put on the “ready to execute” list. Note that only after the thread that has called notify exits the monitor will the thread that was awaken be allowed to check was has changed and enter the monitor![notifyAll() Notifies all waiting threads to check the condition]
Important! wait() and notify() are not like wait() and post() in
semaphores. If notify is called when there is no awaiting thread, it is lost. There is no associated counter, only the means to notify threads that things changed.
47
(A possible) Structure of a monitor
MONITOROnly one thread canbe inside
Blocked threads waiting (ready) to enter the monitor
Blocked threads due to a wait()
Whenever a thread calls wait(),the monitor is released and the
thread is put on the “wait() queue” Whenever a thread calls notify(),one of the threads in the “wait()queue” is transferred to the “ready toenter the monitor” queue
49
Monitors in Unix?
In “C” you don’t have monitors but you haveCONDITION VARIABLES
Condition variables are somewhat similar to monitors. They allow the programmer to suspend a thread until a certain condition is satisfied or notify a thread that a certain condition has changed. The condition can be anything you like!
Counting semaphores are a special type of condition variables The condition is “counter==0”
51
Synchronization – Condition Variables (2)
Important rule: A condition variable always has an associated mutex. Always check the condition variable in mutual exclusion. The
mutex must be locked.
How does it work?
Makes thread Acheck its conditionagain
52
Synchronization – Condition Variables (3)
The thread tests a condition in mutual exclusion. If the condition is false, pthread_cond_wait() atomically releases de mutex AND waits until someone signals that the condition should be tested again.
When the condition is signaled AND the mutex is available, pthread_cond_wait() atomically reacquires the mutex AND releases the thread.
pthread_cond_signal() indicates that exactly one blocked thread should test the condition again. Note that this is note a semaphore. If there is no thread blocked, the “signal” is lost.
If all threads should re-check the condition, use pthread_cond_broadcast(). Since a mutex is involved, each one will test it one at a time, in mutual exclusion.
53
Buffer Cleaner Revised
Suppose a buffer that can hold a maximum of N elements. When it is full, it should immediately be emptied. While the buffer is being emptied, no thread can put things into it.
Producer
CleanerProducer
Producer
57
Condition Variables – Rules (!)
The condition must always be tested with a while loop, never an if! Being unlocked out of a condition variable only means that the condition must be re-checked, not that it has become true
The condition must always be checked and signaled inside a locked mutex.
While condition() may betrue while Thread B is executing,something may happen betweenthe time that the condition is signaledand Thread A is unblock (e.g. anotherthread may change the condition)
58
Classical Problem – Dinning Philosophers
Five philosophers are sitting at a table eating rice from a bowl. For eating rice two chopsticks are needed. There’s only one chopstick on each side of a philosopher.
How to design an algorithm that allows the philosophers to eat without deadlocks, starvation, or live-locking?
59
Dinning Philosophers – Some notes
The following algorithm is bound to deadlock. Why?
The following algorithm may live-lock. Why? (Tip: what happens if all philosophers start to eat at exactly the same time?)
philosopher(i) { wait(chopstick[i]); wait(chopstick[(i+1)%N]); eat(); post(chopstick[(i+1)%N]); post(chopstick[i]);}
philosopher(i) { wait(chopstick[i]); wait(chopstick[(i+1)%N]); eat(); post(chopstick[(i+1)%N]); post(chopstick[i]);}
Try to obtain left chopstick. If successful, obtain right chopstick and eat. If unsuccessful, put down the left chopstick and wait for a while
Try to obtain left chopstick. If successful, obtain right chopstick and eat. If unsuccessful, put down the left chopstick and wait for a while
60
Reference
[Silberschatz05] Chapter 6: Process Synchronization 6.6, 6.7
[Robbins03] Chapter 12: POSIX Threads Chapter 13: Thread Synchronization Chapter 14: Critical Sections and
Semaphores
OperatingSystems
Paulo MarquesDepartamento de Eng. InformáticaUniversidade de [email protected]
2006
/200
7
4. Synchronization4.3. Hardware Issues
63
Bounded-Buffer, from the beginning…Besides putting the CPU to 100% withoutdoing anything useful, why is it wrong???Besides putting the CPU to 100% withoutdoing anything useful, why is it wrong???
64
Some possible problems with ++nElements;
The compiler may generate the following code:
LD R1, @nElementsADD R1, R1, 1SW @nElements, R1
Depending on the interleaving of putItem() and getItem() the final value can be -1, correct, or +1!!!
65
Some possible problems with ++nElements;
Possible solution… Disable the interrupts!
Works on simple processors with non-preemptive kernels.
But, in a (e.g.) dual-core machine, disabling interrupts in one processor does not prevent the other processor from modifying the variable! Non-preemptive kernels: A process executing in kernel
mode cannot be suspended. E.g. Windows XP, Traditional UNIX
Preemptive Kernels: While executing in kernel mode a process can be suspended. E.g. Linux 2.6.XX series and Solaris 10.
CLILD R1, @nElementsADD R1, R1, #1SW @nElements, R1STI
66
Some possible problems with ++nElements;
Even in architectures providing an atomic INC variable:
it may not work!
INC @nElements
The compiler may optimize a tight loop putting the variable in a register. In another process, the variable may be being written to memory, but it’s not visible from the first process. This is especially relevant in multiprocessor machines.
…while (nElements == 0) ;…
…++nElements;…
Process A Process B
a “smart compiler” has put nElements in a register
nElements is being updatedin memory (or a different register)
68
Conditions for solving the critical section problem
Mutual Exclusion Only one process can be
executing in the critical section at a time
Progress If on process is in the
critical section only the processes that are either at the entry or exit sections can be involved in choosing the next process to enter. The decision must be reached in bounded time.
Bounded Waiting No process will starve
indefinitely while trying to enter a critical section in detriment of other processes which repeatedly enter it.
69
Solutions for the Critical Section Problem
Software Algorithms: E.g. Peterson’s Algorithm See Section 6.3 of the book (important!) Complicated to implement and prove correct Very difficult to generalize for several concurrent processes May not work with current hardware
(multi-processor environments with deep speculative out-of-order execution pipelines, having several levels of caches)
In reality, software algorithms implement locks! enter section lock() exit section unlock()
Solutions in hardware Simple “old” single processor machines Disable Interrupts
Even in modern single processor machines may not work TestAndSet and Swap instructions
70
TestAndSet
Instruction that atomically, implemented in hardware, returns the value of a target variable, setting it to TRUE.
Solution for mutual exclusion:
71
Swap
Instruction that atomically swaps the contents of two variables.
Solution for mutual exclusion:
72
Notes on Hardware-Based Support
The two previous algorithms: Solve the mutual exclusion problem and the progress
condition Do not satisfy the bounded waiting condition (why??) Slightly more sophisticated algorithms exist that do so
The implementations shown before are called spin-locks In general they should be avoided Nevertheless, in controlled ways, they are the basis for
implementing true mutexes and semaphores at the operating system level
This is especially relevant on preemptive kernels running on multiprocessors. (Somehow, the kernel must, in a controlled way, allow to signal across processors in mutual exclusion!)
In some high performance applications, sometimes programmers spin-lock for a few moments before blocking on a true lock/semaphore. In this way they are able to prevent a heavy process switch if the resource becomes available in a very short time.
73
Linux 2.6.XX
Linux 2.6 is a fully preemptive kernel. I.e. processes can be suspended while executing in kernel mode.
Basic underlying locking mechanism depends on whether an SMP or non-SMP kernel is being used.
Spin-locks are only used for very short durations. Longer durations imply passing control to a true
lock/semaphore, releasing the processor(s) involved.
single processor multi-processor
Disable kernel preemption Acquire spin-lock
Enable kernel preemption Release spin-lock