0% found this document useful (0 votes)
5 views

5.4.concurrency-semaphore

The document discusses semaphores as a synchronization primitive for managing concurrency in operating systems. It explains the functionality of sem_wait() and sem_post() for controlling access to shared resources, and illustrates their application in problems like the Producer/Consumer scenario. Additionally, it highlights the importance of mutual exclusion to prevent race conditions when multiple threads access shared data.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views

5.4.concurrency-semaphore

The document discusses semaphores as a synchronization primitive for managing concurrency in operating systems. It explains the functionality of sem_wait() and sem_post() for controlling access to shared resources, and illustrates their application in problems like the Producer/Consumer scenario. Additionally, it highlights the importance of mutual exclusion to prevent race conditions when multiple threads access shared data.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 36

Operating Systems

Topic: Concurrency – Semaphores

Endadul Hoque
Acknowledgement

• Youjip Won (Hanyang University)

• OSTEP book – by Remzi and Andrea Arpaci-Dusseau


(University of Wisconsin)
Why study Semaphore?
• Locks and condition variables are both
required to solve different types of
concurrency problems
• Semaphore – a synchronization primitive
– One single primitive for all things related to
synchronization
– Can be used as both locks and condition
variables
Semaphore: A definition
• An object with an integer value
– We can manipulate with two routines;
sem_wait() and sem_post().
– Initialization
1 #include <semaphore.h>
2 sem_t s;
3 sem_init(&s, 0, 1); // initialize s to the value 1

• Declare a semaphore s and initialize it to the value 1


• The second argument, 0, indicates that the semaphore
is shared between threads of the same process.
Semaphore: Interact with semaphore
• sem_wait()[historically known as p()]
1 int sem_wait(sem_t *s) {
2 - decrement the value of semaphore s by one
3 - wait if value of semaphore s is negative
4 }

– If the value of the semaphore was one or higher


when sem_wait()was called, it returns right
away.
– Otherwise, it will cause the caller to suspend
execution waiting for a subsequent post.
– When negative, the value of the semaphore is
equal to the number of waiting threads.
Semaphore: Interact with semaphore
• sem_post()[historically known as v()]
1 int sem_post(sem_t *s) {
2 - increment the value of semaphore s by one
3 - if there are one or more threads waiting, wake one
4 }

– Simply increments the value of the semaphore.


– It does not wait for some particular condition to hold
like sem_wait() does
– If there is a thread waiting to be woken, it wakes one
of them up.
For now, assume actions inside sem_wait() and sem_post() are
performed atomically
Binary Semaphores (Locks)
1 sem_t m;
2 sem_init(&m, 0, X); // initialize semaphore to X; what should X be?
3
4 sem_wait(&m);
5 //critical section here
6 sem_post(&m);

• What should X be?


– The initial value should be 1.
Thread Trace: Single Thread Using A
Semaphore
Value of Semaphore Thread 0 Thread 1
1
1 call sema_wait()
0 sem_wait() returns
0 (crit sect)
0 call sem_post()
1 sem_post() returns
Thread Trace: Two Threads Using A Semaphore
Value Thread 0 State Thread 1 State
1 Running Ready
1 call sem_wait() Running Ready
0 sem_wait() retruns Running Ready
0 (crit set: begin) Running Ready
0 Interrupt; Switch → T1 Ready Running

0 Ready call sem_wait() Running


-1 Ready decrement sem Running
-1 Ready (sem < 0)→sleep sleeping
-1 Running Switch → T0 sleeping
-1 (crit sect: end) Running sleeping
-1 call sem_post() Running sleeping
0 increment sem Running sleeping
0 wake(T1) Running Ready
0 sem_post() returns Running Ready
0 Interrupt; Switch → T1 Ready Running

0 Ready sem_wait() retruns Running


0 Ready (crit sect) Running
0 Ready call sem_post() Running
1 Ready sem_post() returns Running
Thread Trace: Two Threads Using A Semaphore
Value Thread 0 State Thread 1 State
1 Running Ready
1 call sem_wait() Running Ready
0 sem_wait() retruns Running Ready
0 (crit set: begin) Running Ready
0 Interrupt; Switch → T1 Ready Running

0 Ready call sem_wait() Running


-1 Ready decrement sem Running
-1 Ready (sem < 0)→sleep sleeping
-1 Running Switch → T0 sleeping
-1 (crit sect: end) Running sleeping
-1 call sem_post() Running sleeping
0 increment sem Running sleeping
0 wake(T1) Running Ready
0 sem_post() returns Running Ready
0 Interrupt; Switch → T1 Ready Running

0 Ready sem_wait() retruns Running


0 Ready (crit sect) Running
Consider
0 atomic Ready call sem_post() Running
1 Ready sem_post() returns Running
Semaphores As Condition Variables
1 sem_t s;
2
3 void *
4 child(void *arg) {
5 printf("child\n");
6 sem_post(&s); // signal here: child is done
7 return NULL;
s is used for
8 } ordering events
9
10 int
11 main(int argc, char *argv[]) {
12 sem_init(&s, 0, X); // what should X be?
13 printf("parent: begin\n");
14 pthread_t c;
15 pthread_create(c, NULL, child, NULL);
16 sem_wait(&s); // wait here for child
17 printf("parent: end\n"); parent: begin
18 return 0; child
19 } parent: end
A Parent Waiting For Its Child The execution result

– What should X be?


• The value of semaphore should be set to is 0.
Thread Trace: Parent Waiting For Child (Case 1)

• The parent call sem_wait() before the child has called


sem_post().

Value Parent State Child State


0 Create(Child) Running (Child exists; is runnable) Ready
0 call sem_wait() Running Ready
-1 decrement sem Running Ready
-1 (sem < 0)→sleep sleeping Ready
-1 Switch→Child sleeping child runs Running
-1 sleeping call sem_post() Running
0 sleeping increment sem Running
0 Ready wake(Parent) Running
0 Ready sem_post() returns Running
0 Ready Interrupt; Switch→Parent Ready
0 sem_wait() retruns Running Ready
Thread Trace: Parent Waiting For Child (Case 2)

• The child runs to completion before the parent call sem_wait().

Value Parent State Child State


0 Create(Child) Running (Child exists; is runnable) Ready
0 Interrupt; switch→Child Ready child runs Running
0 Ready call sem_post() Running
1 Ready increment sem Running
1 Ready wake(nobody) Running
1 Ready sem_post() returns Running
1 parent runs Running Interrupt; Switch→Parent Ready
1 call sem_wait() Running Ready
0 decrement sem Running Ready
0 (sem≥0)→awake Running Ready
0 sem_wait() retruns Running Ready
The Producer/Consumer (Bounded-Buffer)
Problem
• Producer: put() interface
– Wait for a slot in the buffer to become empty in order to put
data into it.
• Consumer: get() interface
– Wait for a slot to become full before using it.
1 int buffer[MAX]; // MAX # of slots
2 int fill = 0;
3 int use = 0;
4
5 void put(int value) {
6 buffer[fill] = value; // line f1
7 fill = (fill + 1) % MAX; // line f2
8 }
9
10 int get() {
11 int tmp = buffer[use]; // line g1
12 use = (use + 1) % MAX; // line g2
13 return tmp;
14 }
The Producer/Consumer (Bounded-Buffer)
Problem
1 sem_t empty;
2 sem_t full;
3
4 void *producer(void *arg) {
5 int i;
6 for (i = 0; i < loops; i++) {
7 sem_wait(&empty); // line P1
8 put(i); // line P2
9 sem_post(&full); // line P3
10 }
11 }
12
13 void *consumer(void *arg) {
14 int i, tmp = 0;
15 for (i = 0; i < loops; i++) {
16 sem_wait(&full); // line C1
17 tmp = get(); // line C2
18 sem_post(&empty); // line C3
19 printf("%d\n", tmp);
20 }
21 }
22 …

First Attempt: Adding the Full and Empty Conditions


The Producer/Consumer (Bounded-Buffer)
Problem
21 int main(int argc, char *argv[]) {
22 // …
23 sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with…
24 sem_init(&full, 0, 0); // … and 0 are full
25 // …
26 }
First Attempt: Adding the Full and Empty Conditions (Cont.)

• empty and full indicate # of the slots empty or


full, respectively
• No issues when MAX = 1
– Behaves like condition variables
The Producer/Consumer (Bounded-Buffer)
Problem
21 int main(int argc, char *argv[]) {
22 // …
23 sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with…
24 sem_init(&full, 0, 0); // … and 0 are full
25 // …
26 }
First Attempt: Adding the Full and Empty Conditions (Cont.)

– Imagine that MAX is greater than 1 .


• If there are multiple producers, race condition can happen at line
f1.
• It means that the old data in a slot can be overwritten.
– This code fails to ensure mutual exclusion.
• The filling of a buffer and incrementing of the index into the
buffer is a critical section.
– A similar race condition can occur if there’s multiple
consumers; the code inside get() is a critical section
A Solution: Adding Mutual Exclusion
1 sem_t empty;
2 sem_t full;
3 sem_t mutex;
4
5 void *producer(void *arg) {
6 int i;
7 for (i = 0; i < loops; i++) {
8 sem_wait(&mutex); // line p0 (NEW LINE)
9 sem_wait(&empty); // line p1
10 put(i); // line p2
11 sem_post(&full); // line p3
12 sem_post(&mutex); // line p4 (NEW LINE)
13 }
14 }
15
(Cont.)

Adding Mutual Exclusion (Incorrectly)


A Solution: Adding Mutual Exclusion

(Cont.)
16 void *consumer(void *arg) {
17 int i;
18 for (i = 0; i < loops; i++) {
19 sem_wait(&mutex); // line c0 (NEW LINE)
20 sem_wait(&full); // line c1
21 int tmp = get(); // line c2
22 sem_post(&empty); // line c3
23 sem_post(&mutex); // line c4 (NEW LINE)
24 printf("%d\n", tmp);
25 }
26 }

Adding Mutual Exclusion (Incorrectly)


A Solution: Adding Mutual Exclusion
• Imagine two thread: one producer and one
consumer.
– The consumer acquire the mutex (line c0).
– The consumer calls sem_wait() on the full
semaphore (line c1).
– The consumer is blocked and yield the CPU.
• The consumer still holds the mutex!
– The producer calls sem_wait() on the binary mutex
semaphore (line p0).
– The producer is now stuck waiting too → a classic
deadlock.
Finally, A Working Solution
1 sem_t empty;
2 sem_t full;
3 sem_t mutex;
4
5 void *producer(void *arg) {
6 int i;
7 for (i = 0; i < loops; i++) {
8 sem_wait(&empty); // line p1
9 sem_wait(&mutex); // line p1.5 (MOVED MUTEX HERE…)
10 put(i); // line p2
11 sem_post(&mutex); // line p2.5 (… AND HERE)
12 sem_post(&full); // line p3
13 }
14 }
15
(Cont.)

Adding Mutual Exclusion (Correctly)


Finally, A Working Solution
1 sem_t empty;
2 sem_t full; Use mutex just around the
3 sem_t mutex;
4
critical section
5 void *producer(void *arg) {
6 int i;
7 for (i = 0; i < loops; i++) {
8 sem_wait(&empty); // line p1
9 sem_wait(&mutex); // line p1.5 (MOVED MUTEX HERE…)
10 put(i); // line p2
11 sem_post(&mutex); // line p2.5 (… AND HERE)
12 sem_post(&full); // line p3
13 }
14 }
15
(Cont.)

Adding Mutual Exclusion (Correctly)


Finally, A Working Solution
(Cont.)
16 void *consumer(void *arg) {
17 int i;
18 for (i = 0; i < loops; i++) {
19 sem_wait(&full); // line c1
20 sem_wait(&mutex); // line c1.5 (MOVED MUTEX HERE…)
21 int tmp = get(); // line c2
22 sem_post(&mutex); // line c2.5 (… AND HERE)
23 sem_post(&empty); // line c3
24 printf(“%d\n”, tmp);
25 }
26 }
27
28 int main(int argc, char *argv[]) {
29 // …
30 sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with…
31 sem_init(&full, 0, 0); // ... and 0 are full
32 sem_init(&mutex, 0, 1); // mutex=1 because it is a lock
33 // …
34 }

Adding Mutual Exclusion (Correctly)


Finally, A Working Solution
(Cont.)
16 void *consumer(void *arg) { Use mutex just around the
17 int i; critical section
18 for (i = 0; i < loops; i++) {
19 sem_wait(&full); // line c1
20 sem_wait(&mutex); // line c1.5 (MOVED MUTEX HERE…)
21 int tmp = get(); // line c2
22 sem_post(&mutex); // line c2.5 (… AND HERE)
23 sem_post(&empty); // line c3
24 printf(“%d\n”, tmp);
25 }
26 }
27
28 int main(int argc, char *argv[]) {
29 // …
30 sem_init(&empty, 0, MAX); // MAX buffers are empty to begin with…
31 sem_init(&full, 0, 0); // ... and 0 are full
32 sem_init(&mutex, 0, 1); // mutex=1 because it is a lock
33 // …
34 }

Adding Mutual Exclusion (Correctly)


Reader-Writer Locks
• Imagine several concurrent list operations,
including inserts and simple lookups.
– insert:
• Change the state of the list
• Thus, it’s a traditional critical section
– lookup:
• Simply read the data structure.
• As long as we can guarantee that no insert is on-going, we
can allow many lookups to proceed concurrently.

This special type of lock is known as a reader-writer lock.


Reader-Writer Locks
• Only a single writer can acquire the lock.
• Once a reader has acquired a read lock,
– More readers will be allowed to acquire the read lock too.
– A writer will have to wait until all readers are finished.
1 typedef struct _rwlock_t {
2 sem_t lock; // binary semaphore (basic lock)
3 sem_t writelock; // used to allow ONE writer or MANY readers
4 int readers; // count of readers reading in critical section
5 } rwlock_t;
6
7 void rwlock_init(rwlock_t *rw) {
8 rw->readers = 0;
9 sem_init(&rw->lock, 0, 1);
10 sem_init(&rw->writelock, 0, 1);
11 }
12
Reader-Writer Locks
13. void rwlock_acquire_readlock(rwlock_t *rw) {
14. sem_wait(&rw->lock);
15 rw->readers++;
16 if (rw->readers == 1)
17 sem_wait(&rw->writelock); // first reader acquires writelock
18 sem_post(&rw->lock);
19 }
20
21 void rwlock_release_readlock(rwlock_t *rw) {
22 sem_wait(&rw->lock);
23 rw->readers--;
24 if (rw->readers == 0)
25 sem_post(&rw->writelock); // last reader releases writelock
26 sem_post(&rw->lock);
27 }
28 void rwlock_acquire_writelock(rwlock_t *rw) {
29 sem_wait(&rw->writelock);
30 }
31 void rwlock_release_writelock(rwlock_t *rw) {
32 sem_post(&rw->writelock);
33 }
Reader-Writer Locks
• The reader-writer locks have fairness problem.
– It would be relatively easy for reader to starve writer.
• The reader-writer locks should be used with
some caution
– They add more overhead
The Dining Philosophers
• Assume there are five “philosophers” sitting around a table.
– Between each pair of philosophers is a single fork (five total).
– The philosophers each have times where they think, and don’t need any
forks, and times where they eat.
– In order to eat, a philosopher needs two forks, both the one on their left
and the one on their right.
P1
• Why study? f2 f1

– The contention for these forks.


– The underlying synchronization P2 P0
problems

f3 f0

P3 P4
f4
The Dining Philosophers
• Key challenges
– There is no deadlock.
– No philosopher starves and never gets to eat.
– Concurrency is high.

while (1) { // helper functions


think(); int left(int p) { return p; }
getforks();
eat(); int right(int p) {
putforks(); return (p + 1) % 5;
} }
Basic loop of each philosopher Helper functions (Downey’s solutions)

Philosopher p wishes to refer to the fork on their left → call left(p).


Philosopher p wishes to refer to the fork on their right → call right(p).
The Dining Philosophers
• We need some semaphore, one for each fork: sem_t forks[5].
1 void getforks() {
2 sem_wait(forks[left(p)]);
3 sem_wait(forks[right(p)]);
4 }
5
6 void putforks() {
7 sem_post(forks[left(p)]);
8 sem_post(forks[right(p)]);
9 }
The getforks() and putforks() Routines (Broken Solution)

• Deadlock occurs!
– If each philosopher happens to grab the fork on their left before
any philosopher can grab the fork on their right.
– Each will be stuck holding one fork and waiting for another, forever.
A Solution: Breaking The Dependency
• Change how forks are acquired.
– Let’s assume that philosopher 4 acquire the forks in a
different order.
1 void getforks() {
2 if (p == 4) {
3 sem_wait(forks[right(p)]);
4 sem_wait(forks[left(p)]);
5 } else {
6 sem_wait(forks[left(p)]);
7 sem_wait(forks[right(p)]);
8 }
9 }

• There is no situation where each philosopher grabs one fork


and is stuck waiting for another. The cycle of waiting is
broken.
How To Implement Semaphores
• Build our own version of semaphores called Zemaphores
1 typedef struct __Zem_t {
2 int value;
3 pthread_cond_t cond;
4 pthread_mutex_t lock;
5 } Zem_t;
6
7 // only one thread can call this
8 void Zem_init(Zem_t *s, int value) {
9 s->value = value;
10 Cond_init(&s->cond);
11 Mutex_init(&s->lock);
12 }
13
14 void Zem_wait(Zem_t *s) {
15 Mutex_lock(&s->lock);
16 while (s->value <= 0)
17 Cond_wait(&s->cond, &s->lock);
18 s->value--;
19 Mutex_unlock(&s->lock);
20 }
21 …
How To Implement Semaphores
22 void Zem_post(Zem_t *s) {
23 Mutex_lock(&s->lock);
24 s->value++;
25 Cond_signal(&s->cond);
26 Mutex_unlock(&s->lock);
27 }

– Zemaphores don’t maintain the invariant that the


value of the semaphore, when negative, reflects
the number of waiting threads
• The value never be lower than zero. – Why?
– Condition at Line 16 is checked before decrementing the value

• This behavior is easier to implement and matches the


current Linux implementation.
Reading Material

• Chapter 31 of OSTEP book – by Remzi and Andrea


Arpaci-Dusseau (University of Wisconsin)
https://pages.cs.wisc.edu/~remzi/OSTEP/threads-
sema.pdf
Questions?

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy