Home | Projects | Notes > Multi-Threading (POSIX Threads) > Thread Synchronization - Semaphore
Proposed by Edsger Dijkstra, technique to manage concurrent threads by using a simple integer value which is known as a semaphore.
A sempahore allows at most
Here,
We have an assumption that the critical section protected by a semaphore is of a special property that it can be executed by
Semaphores can be used to synchronize multiple processes running on the same machine as well as to synchronize multiple threads within a process.
Semaphore APIs
xxxxxxxxxx
181
2
3/* declare semaphore variable */
4sem_t sem;
5
6/* initialize the semaphore */
7sem_init(sem_t *sem, /* pointer to the semaphore to be initialized */
8 int pshared, /* pshared = 0 to synchronize threads, non-zero value to synchronize processes */
9 int permit_counter); /* should be initialized to a non-negative integer */
10
11/* blocking the calling thread if semaphore is not available */
12sem_wait(sem_t *sem);
13
14/* signal the blocked thread on semaphore */
15sem_post(sem_t *sem);
16
17/* destroy the semaphore after use */
18sem_destroy(sem_t *sem);
sem_wait()
andsem_post()
forms the core functionality of a semaphore; Allow at mostthreads to execute concurrently in a critical section.
sem_wait(sem_t *sem);
Unconditionally decrease the
permit_counter
of the semaphore.If
permit_counter
< 0 after decrement, block the calling thread.
sem_post(sem_t *sem);
Unconditionally increase the
permit_counter
of the semaphore.Send signal to threads blocked on
sem_wait()
, if any. If there is no thread blocked on the semaphore, signal will not be sent. (See the implementation)This signal will be delivered to any thread which is blocked on the semaphore by the OS and the recipient thread of the signal will then executesem_wait()
and enter the critical section.
When permit_counter
= 1, it is called a binary semaphore which is basically the same as mutex. The only difference is that a binary semaphore can be unblocked(= sem_post()
) by a different thread whereas a mutex can only be unlocked by the thread that has locked it. In other words, the order in which the threads invoke sem_wait()
does not necessarily match the order in which the threads invoke sem_post()
. It is indeterministic!
My understanding on the difference between a binary semaphore and a mutex is not clear! Revisit these concepts and clarify them! (Mutex - lock/unlock, Semaphore - wait/signal)
Write a program that creates 5 threads, allows at most 2 threads to execute in the critical section concurrently. You may face the critical section by just printing some messages by the executing threads.
xxxxxxxxxx
871/*
2 * File Name : semaphore_hello_world.c
3 * Description : C program to demonstrate the usage of a semaphore to limite the number of threads
4 * running inside the critical section.
5 * Author : Modified by Kyungjae Lee
6 * (Original: Abhishek Sagar - Juniper Networks)
7 * Date Created : 01/13/2023
8 */
9
10/*
11 * compile using:
12 * gcc -g -c semaphore_hello_world.c -o semaphore_hello_world.o
13 * gcc -g semaphore_hello_world.o -o semaphore_hello_world -lpthread
14 *
15 * run using:
16 * ./semaphore_hello_world
17 */
18
19
20
21/* POSIX threads*/
22/* pause(), sleep() */
23/* global variable errno */
24/* semaphore */
25
26sem_t sem;
27pthread_t threads[5];
28
29
30
31/* A thread callback fn must have following prototypes
32 * void* (*thread_fn)(void *)
33 */
34static void* thread_fn_callback(void *arg)
35{
36 char *thread_name = (char *)arg;
37
38 int i;
39
40 printf("%s attempts to enter C.S\n", thread_name);
41
42 sem_wait(&sem);
43
44 /* CS Begins here */
45 printf("%s sucessfully entered C.S\n", thread_name);
46
47 for (i = 0 ; i < 5; i++)
48 {
49 printf("%s executing in C.S\n", thread_name);
50 sleep(1);
51 }
52
53 printf("%s exiting from C.S\n", thread_name);
54 /* CS Ends here */
55
56 sem_post(&sem); /* analogous to 'pthread_cond_singnal()' */
57
58 printf("%s completely exited from C.S\n", thread_name);
59}
60
61void thread_create(pthread_t *thread_handle, void *arg)
62{
63 int rc = pthread_create(thread_handle, NULL, thread_fn_callback, arg);
64 if(rc != 0)
65 {
66 printf("Error occurred, thread could not be created, errno = %d\n", rc);
67 exit(0);
68 }
69}
70
71int main(int argc, char **argv)
72{
73 sem_init(&sem, 0, PERMIT_COUNT);
74 thread_create(&threads[0], "thread0");
75 thread_create(&threads[1], "thread1");
76 thread_create(&threads[2], "thread2");
77 thread_create(&threads[3], "thread3");
78 thread_create(&threads[4], "thread4");
79
80 int i ;
81 for (i = 0; i < 5; i++)
82 {
83 pthread_join(threads[i], NULL);
84 }
85 sem_destroy(&sem);
86 return 0;
87}
xxxxxxxxxx
301thread0 attempts to enter C.S
2thread0 sucessfully entered C.S
3thread0 executing in C.S
4thread3 attempts to enter C.S
5thread3 sucessfully entered C.S
6thread2 attempts to enter C.S
7thread3 executing in C.S
8thread4 attempts to enter C.S
9thread1 attempts to enter C.S
10thread3 executing in C.S
11thread0 executing in C.S
12thread3 executing in C.S
13thread0 executing in C.S
14thread3 executing in C.S
15thread0 executing in C.S
16thread3 executing in C.S
17thread0 executing in C.S
18thread3 exiting from C.S
19thread3 completely exited from C.S
20thread0 exiting from C.S
21thread2 sucessfully entered C.S
22thread2 executing in C.S
23thread4 sucessfully entered C.S
24thread4 executing in C.S
25thread0 completely exited from C.S
26thread2 executing in C.S
27thread4 executing in C.S
28thread2 executing in C.S
29thread4 executing in C.S
30...
Notice that at most 2 threads are executing in the critical section concurrently.
Strict alternation is nothing but a pattern of execution of threads.
It is of practical significance to make a pair of threads to execute in strict alternation manner. (It is about two threads!)
Strict alternation can be implemented using zero semaphores whose permit_counter
is 0.
xxxxxxxxxx
21sem_wait(&sem); /* calling thread is immediately blocked */
2sem_post(&sem); /* blocked thread, if any, wakes up */
Any thread (T2) in the system can invoke
sem_post()
API on the same semaphore T1 was blocked on.
Write a program which create two threads T1 and T2. T1 thread prints odd numbers between 1 to 15. T2 prints even numbers between 2 to 15. Output should be sequential.
xxxxxxxxxx
71/* T1 */
2for (i = 1; i < 15; i += 2)
3{
4 print i;
5 sem_post(sem0_1);
6 sem_wait(sem0_2);
7}
xxxxxxxxxx
71/* T2 */
2for (i = 2; i < 15; i += 2)
3{
4 sem_post(sem0_1);
5 print i;
6 sem_wait(sem0_2);
7}
Mutex and condition variable are building blocks of semaphores.
In fact, mutex and condition variable are building blocks for all thread synchronization data structures such as thread barriers, wait queues, thread pools, monitors, thread schedulers, etc.
Understanding the integer variable permit_counter
:
When permit_counter
The number of threads which are allowed to enter the critical section without waiting.
When permit_counter
The number of threads which are blocked from entering the critical section because the maximum allowed limit has reached.
Interface for Semaphore
xxxxxxxxxx
351/*
2 * File Name : semaphore.h
3 * Description : Interface for semaphore
4 * Author : Modified by Kyungjae Lee (Original: Abhishek Sagar - Juniper Networks)
5 * Date Created : 01/13/2023
6 */
7
8
9
10
11
12
13typedef struct sema_
14{
15 int permit_counter;
16 pthread_cond_t cv;
17 pthread_mutex_t mutex;
18} sema_t;
19
20sema_t* sema_get_new_semaphore();
21void sema_init(sema_t *sema, int count);
22void sema_wait(sema_t *sema);
23void sema_post(sema_t *sema);
24void sema_destroy(sema_t *sema);
25int sema_getvalue(sema_t *sema);
26
27/* several texts using P & V notation to represent wait and signals */
28
29
30
31/* some texts also use up and down */
32
33
34
35/* SEMAPHORE_H */
The member variable
mutex
is used to lock and unlock the section of code wherepermit_counter
is getting updated.The member variable
cv
is used when a thread needs to wait or block on the semaphore.
Implementation of Semaphore
xxxxxxxxxx
661/*
2 * File Name : semaphore.c
3 * Description : Implementation of semaphore
4 * Author : Modified by Kyungjae Lee (Original: Abhishek Sagar - Juniper Networks)
5 * Date Created : 01/13/2023
6 */
7
8
9
10
11
12
13
14sema_t* sema_get_new_semaphore()
15{
16 sema_t *sema = calloc(1, sizeof(sema_t));
17 return sema;
18}
19
20void sema_init(sema_t *sema, int permit_counter)
21{
22 sema->permit_counter = permit_counter;
23 pthread_cond_init(&sema->cv, NULL);
24 pthread_mutex_init(&sema->mutex, NULL);
25}
26
27/* without using pending signals */
28void sema_wait(sema_t *sema)
29{
30 pthread_mutex_lock(&sema->mutex);
31 sema->permit_counter--;
32 if (sema->permit_counter < 0)
33 {
34 pthread_cond_wait(&sema->cv, &sema->mutex); /* mutex will be unlocked behind the scenes */
35 }
36 pthread_mutex_unlock(&sema->mutex);
37}
38
39void sema_post(sema_t *sema)
40{
41 bool any_thread_waiting;
42 pthread_mutex_lock(&sema->mutex);
43
44 any_thread_waiting = sema->permit_counter < 0 ? true : false;
45 sema->permit_counter++;
46
47 if (any_thread_waiting)
48 {
49 pthread_cond_signal(&sema->cv); /* one of the blocked threads chosen by thew OS
50 will get the signal */
51 }
52 pthread_mutex_unlock(&sema->mutex);
53}
54
55void sema_destroy(sema_t *sema)
56{
57 sema->permit_counter = 0;
58 pthread_mutex_unlock(&sema->mutex);
59 pthread_cond_destroy(&sema->cv);
60 pthread_mutex_destroy(&sema->mutex);
61}
62
63int sema_getvalue(sema_t *sema)
64{
65 return sema->permit_counter;
66}
To check if your own implementation of a semaphore is robust, run it with the modified version of the previous demo program.
Types of Semaphores:
Unnamed Semaphores
Used to synchronize threads or related processes (i.e., processes that are created by fork()
system call)
We've been discussing about unnamed semaphores till now.
Named Semaphores
Mainly used for synchronizing unrelated processes, but can also be used for synchronizing threads or related processes.
Have names using which the semaphore can be uniquely identified in the system.
Weak Semaphore
Whenever theoretically you can show in your solution that some thread blocked on a mutex or semaphore may never get a chance to resume its execution (Starvation), then we say that solution is lacking the property of Bounded Waiting and such a semaphore is called Weak Semaphore.
The root cause-of the weak semaphore is the randomness of scheduling which thread to allow to enter the critical section next.
Strong Semaphore
We can deploy a way such that blocked threads are unblocked in a FIFO way per signal (or in any other way that can prevent the starvation), then such a semaphore is called the Strong Semaphore. Bounded Waiting is guaranteed in strong semaphores.
Weak/strong is the topic discussed in the realm of theory because in reality, it is very rare for a starvation of threads to occur.
Bounded waiting means that any particular thread blocked on a semaphore would have a finite amount of waiting time. It is guaranteed that at some point in time, a particular thread blocked on a semaphore will be scheduled and enter the critical section. It is a desirable property that any synchronization solution must have.
Weak semaphores can be converted into strong semaphores by changing the policy of blocked thread selection from random to FIFO (sequel course).
The semaphores we have dealt with so far (i.e., custom semaphore, built-in semaphore) are all "weak semaphores". Even the mutexes are "weak" in this sense since the blocked threads are randomly selected by the operating system.
Sagar, A. (2022). Part A - Multithreading & Thread Synchronization - Pthreads [Video file]. Retrieved from https://www.udemy.com/course/multithreading_parta/