What's wrong with my code?
Multiple things, largely discussed in comments already. The most significant one is that your code is rife with data races. That is one of the things that semaphores are often used to protect against, but in this case it is your semaphores themselves that are racy. Undefined behavior results.
Additional issues include
- A pthreads thread function must return
void *
, but yours returns void
- You overrun the bounds of your
main()
's ps
and threads
arrays
- You do not join your threads before the program exits.
- You define functions with the same names as a C standard library function (
signal()
) and a standard POSIX function (wait()
).
Nevertheless, if your C implementation supports the atomics option then you can use that to implement a working semaphore not too different from your original code:
#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
// This is used only for defining the enum constants
enum sem_val { NOT_IN_USE, IN_USE };
// It is vital that sem be *atomic*.
// With `stdatomic.h` included, "_Atomic int" could also be spelled "atomic_int".
_Atomic int sem = ATOMIC_VAR_INIT(NOT_IN_USE);
struct parameters{
int thread_num;
};
void my_sem_wait() {
int expected_state = NOT_IN_USE;
// See discussion below
while (!atomic_compare_exchange_strong(&sem, &expected_state, IN_USE)) {
// Reset expected_state
expected_state = NOT_IN_USE;
}
}
void my_sem_signal() {
// This assignment is performed atomically because sem has atomic type
sem = NOT_IN_USE;
}
void *resource(void *params) {
//assuming parameter is a parameters struct.
struct parameters *p = params;
my_sem_wait();
printf("Resource is being used by thread %d\n", p->thread_num);
my_sem_signal();
return NULL;
}
int main(void) {
pthread_t threads[4];
struct parameters ps[4] = {{1},{2},{3},{4}};
for (int counter = 0; counter < 4; counter++) {
pthread_create(&threads[counter], NULL, resource, &ps[counter]);
}
// It is important to join the threads if you care that they run to completion
for (int counter = 0; counter < 4; counter++) {
pthread_join(threads[counter], NULL);
}
return 0;
}
Most of that is pretty straightforward, but the my_sem_wait()
function bears a little more explanation. It uses an atomic compare-and-swap to make sure that threads proceed only if they change the value of the semaphore from NOT_IN_USE
to IN_USE
, with the comparison and conditional assignment being performed as a single atomic unit. Specifically, this ...
atomic_compare_exchange_strong(&sem, &expected_state, IN_USE)
... says "Atomically, compare the value of sem
to the value of expected_state
and if they compare equal then assign value IN_USE
to to sem
." The function additionally sets the value of expected_state
to the one read from sem
if they differ, and returns the result of the equality comparison that was performed (equivalently: returns 1 if the specified value was assigned to sem
and 0 if not).
It is essential that the comparison and swap be performed as an atomic unit. Individual atomic reads and writes would ensure that there is no data race, but they would not ensure correct program behavior, because two threads waiting on the semaphore could both see it available at the same time, each before the other had a chance to mark it unavailable. Both would then proceed. The atomic compare and swap prevents one thread reading the value of the semaphore between another's read and update of that value.
Do note, however, that unlike a pthreads mutex or a POSIX semaphore, this semaphore waits busily. That means threads waiting to acquire the semaphore consume CPU while they do, rather than going to sleep as threads waiting on a pthreads mutex do. That may be ok if semaphore access is usually uncontended or if threads never hold it locked very long, but under other circumstances it can make your program much more resource-hungry than it needs to be.