13 - Semaphores
Class: CSCE-313
Notes:
What is the full producer-consumer problem?
- The producer-consumer problem is a problem in which producers generate data and consumers retrieve and use the data concurrently.
- The producers and consumers share the same fixed-sized memory buffer.
- Earlier we looked at a version of the producer-consumer problem in which the producer never waits
- Can we extend it to the case when both need to wait?
Semaphores (counting)
A semaphore is a non-negative integer variable, shared among multiple processes. They are used for access control to a common resource in a concurrent environment.
A Semaphore supports the following two atomic operations:
wait(): Waits until value, then decrements it by 1 - Think of this as the
wait()operation, or thelock()operation
- Think of this as the
post(): Increments value by 1- May wake up a waiting thread
- Analogous to the
signal()function, or theunlock()operation.
/CSCE-313/Lecture/Visual%20Aids/image-37.png)
Notes:
- Think of it as an atomic integer with non-negative values
- One use-case is being able to model a room with a fixed capacity
- IF you initialize the semaphore with a value of 100, then every time somebody enters the room, it decrements the variable.
- If the value of the variable is 0, then they are blocked
- Example:
- It will let 100 people eneter
- The next guy that calls wait() on the semapahore, it will block
- Think of it as being an atomic integer, except that you cannot go below 0, you will block at 0, and you are waiting for somebody to increment it.
- When you wait() on the variable, you basically try to decrement the value of the semaphore, if that value is greater than 0, if it is, you decrement it and you acquire the semaphore
- There will be many waiters, when somebody leaves, it basically signals (pauses) the semaphores, and then one of the n people waiting outside will be permitted to enter and acquire the semaphore
Question on midterm
Someone that din't want to use swapcontext
// Switch context from CurrentThread to next_thread
void thread_switch (thread_t *next_thread) {
getcontext (&CurrentThread->ctx);
CurrentThread = next_thread;
setcontext (&CurrentThread->ctx);
return;
}
- Later when you are going to be restored, you are going to return from the getcontext() call, so you will once again switch to that of next_thread.
- The problem is that you need to be able to figure out that his is the first time you are actually setting context.
Correct answer:
// Switch context from CurrentThread to next_thread
void thread_switch (thread_t *next_thread) {
volatile bool first_time = true;
getcontext (&CurrentThread->ctx);
if(first_time) {
first_time = false;
CurrentThread = next_thread;
setcontext (&CurrentThread->ctx);
return;
}
}
- Now before you switch to the other guy, you set the
first_timevariable to false - Now when you return from
getcontext()you will find thatfirst_timeis false so you won't change to it. - If you mark
first_timeas volatile, then it is not loaded into a registers, it is kept in memory only
Counting Semaphores
- -1 on entry
- entangled. +1 on exit
These are entangled versions of the same counter but they are atomic. The semaphore allows you to be able to control the number of people in the theater. Once this value becomes 0, they will block.
Semaphores
A semaphore is like a key hat allows a task to carry out some operation or to access a resource.
The only operations allowed are wait() and post().
Operations must be atomic
- Two
wait()s together cannot decrement the value below zero. - Similarly, a thread going to sleep in
wait()won't miss wakeup from post, even if they both happen at the "same" time.
/CSCE-313/Lecture/Visual%20Aids/image-40.png)
Semaphore Implementation
/CSCE-313/Lecture/Visual%20Aids/image-41.png)
class Semaphore{
private:
int value;
mutex m;
condition_variable cv;
public:
Semaphore (int _v):value(_v){}
void wait(){
unique lock<mutex> l(m);
// wait until the value is positive
cv.wait(l, [this]{return value > 0;});
value--;
}
void post(){
unique_lock<mutex>
value++;
cv.notify_one();
}
};
- Wait until value>0, so it can be decremented. Then decrement it.
- Always notify on the way out, notify_all() is correct, but would lead to spurious wake ups
Notes:
- When you do notify_one() you are guaranteed that any waiter that is picked up, will be able to complete the task
Semaphores: the signal pattern
/CSCE-313/Lecture/Visual%20Aids/image-42.png)
Notes:
- You can do this using condition variables, but lets look at the semaphore implementation:
- It is initialize to 0
- Thread 2 is waiting for Thread 1 to finish something.
- If Thread 2 executes first, it will block
- If Thread 1 executes first and calls
post, it is not going to be lost, it will increment the counter.- Value of Semaphore becomes 1
- Later on when Thread 2 runs it will find that the condition is true
- The thing about semaphores is that there are some problems where using semaphore makes code a lot cleaner and easier but everything done by semaphores can already be done with condition variables
Code Semaphores: the signal pattern
sem_t sem;
void *thread (void *arg) {
printf("Child working...");
fflush(stdout);
sleep(4);
printf("finished.\n");
sleep(1);
sem_post(&sem);
}
int main () {
sem_init(&sem, 0, 0);
pthread_t t1;
pthread_create(&t1, NULL, thread, NULL);
sem_wait(&sem);
printf("Parent received signal from child.\n");
pthread_join(t1, NULL);
sem_destroy(&sem);
return 0;
}
Two Uses of Semaphores
- For mutual exclusion (initial value = 1). Also called "Binary Semaphore".
semaphore.wait();
// Critical section goes here
semaphore.post();
- For scheduling constraints (initial value
). Allow thread 1 to wait for a signal from thread 2 , i.e., thread 2 schedules thread 1 when a given constraint is satisfied.
Ex: Suppose you had to implement ThreadJoin which must wait for a thread to terminate:
// Initial value of semaphore = 0
ThreadJoin { // the joiner calls this
semaphore.wait();
}
ThreadFinish { // the joinee calls this
semaphore.post();
}
Notes:
- In mutexes, you can only release the mutex you hold
- But here, anyone can post to the semaphore so you have to be careful
Producer-consumer with a bounded buffer
Problem Definition
- A producer puts data into a shared buffer
- A consumer takes them out
- We need synchronization to coordinate between producer/consumer
We do not want the producer and consumer to have to work in lockstep (buffer size 1), so we put a fixed-size buffer between them.
- We need to synchronize access to this buffer
- The producer needs to wait if buffer is full (e.g refill a vending machine)
- The consumer needs to wait if buffer is empty (e.g. customer waits if the machine is empty)
/CSCE-313/Lecture/Visual%20Aids/image-43.png)
Notes:
- We want to encode the free space in the buffer, using a semaphore
Correctness constraints
...
Semaphores on bounded buffer
- The producer decreases the number of empty slots and increases the number of occupied slots:
- emptySlots.wait(), fullSlots.post()
- The consumer decreases the number of occupied slots and increases the number of empty slots:
- fullSlots.wait(), emptySlots.post()
One thread is creating space, the other one is filling space.
/CSCE-313/Lecture/Visual%20Aids/image-46.png)
Notes:
- When a producer is adding an item, they need an index
- Sync is just a thing for accessing this index
Full Solution to Bounded Buffer
/CSCE-313/Lecture/Visual%20Aids/image-44.png)
Semaphore fullSlots = 0; // Initially, no coke
Semaphore emptySlots = bufSize; //Initially all empty
Semaphore sync = 1; // No one is using machine
Producer(item) {
emptySlots.wait(); // Once acquired, never violated
sync.wait(); // Wait until machine is free
enqueue(item);
sync.post();
fullSlots.post(); // Notify there is more coke
}
Consumer() {
fullSlots.wait(); // Check if there's a coke
sync.wait(); // Wait until machine free
item = dequeue();
sync.post();
emptySlots.post(); // tell producer to produce more
return item;
}
Notes:
- Once acquiring the semaphore, the condition that led to that will not change
- Now that number that decremented is owned by you, you own an empty slot in the semaphore
- The consumer does the same thing
- The moment it knows there is somehting in the buffer, then it knows it will always be able to pull from this buffer
- You do not need any more atomicity, you know one of them is subscribed to you
- Then increment empty space
More Thoughts
Is order of wait’s important?
Yes. because of deadlocks
/CSCE-313/Lecture/Visual%20Aids/image-45.png)
Notes:
- If you were waiting for a slot to become empty, then you may never actually get it because you own the mutual exclusion semaphore, and your consumer will never be able to have it
- Sync
Is order of posts important? No, except that it might affect scheduling efficiency.
/CSCE-313/Lecture/Visual%20Aids/image-47.png)
What if we have 2 producers or 2 consumers? Do we need to change anything?
No.
Producer(item) {
emptySlots.wait();
sync.wait();
Enqueue(item);
sync.post();
fullSlots.post();
}
Consumer() {
fullSlots.wait();
sync.wait();
item = Dequeue();
sync.post();
emptySlots.post();
return item;
}
The rendezvous pattern
Generalize the "signal" pattern so that it works both ways. Thread A has to wait for Thread B and vice versa. In other words, given this code
/CSCE-313/Lecture/Visual%20Aids/image-48.png)
We want to guarantee that
Notes:
- You want to implement a barrier to wait for each other's Thread
- In other words you want one signal like the blue arrow and one signal like the red arrow
- We have two semaphores and they are basically signaling each other