Home > front end >  Binary Semaphore program has extremely unpredictable output
Binary Semaphore program has extremely unpredictable output

Time:10-07

My program uses an enum as a semaphore. There are two possible values/states(Because it's a binary semaphore). The program compiles fine. signal() and wait() look logical. Why is program behavior so unpredictable? Even the integer printf is buggy. Here's the code:

#include <stdio.h>
#include <pthread.h>
typedef enum {IN_USE,NOT_IN_USE} binary_semaphore;
binary_semaphore s=NOT_IN_USE;
struct parameters{
    int thread_num;
};
void wait(){
    while(s==IN_USE);
    s=IN_USE;
}
void signal(){
    s=NOT_IN_USE;
}
void resource(void *params){
    //assuming parameter is a parameters struct.
    struct parameters *p=(struct parameters*)params;
    wait();
    printf("Resource is being used by thread %d\n",(*p).thread_num);
    signal();
}
int main(void){
    pthread_t threads[4];
    struct parameters ps[4]={{1},{2},{3},{4}};
    register int counter=0;
    while(counter  <4){
        pthread_create(&threads[counter],NULL,resource,(void*)&ps[counter]);
    }
    return 0;
}

What's wrong with my code? Some of the outputs(Yes, they're different every time):-

(NOTHING)
Resource is being used by thread 32514
Resource is being used by thread 0
Resource is being used by thread 0
Resource is being used by thread 32602
Resource is being used by thread -24547608

Is it a garbage value issue?

CodePudding user response:

You are encountering race conditions as well as undefined behavior. When you use a regular int as a semaphore in multithreaded applications, there's no guarantee that a process will read a variable's value and modify it before another process is able to read it, which is why you must use a concurrency library designed for your operating system. Furthermore, the function pointer you passed to pthread_create is not the right type, which is undefined behavior.

I replaced your "semaphore" enum with a pointer to pthread_mutex_t which is initialized on the stack of main(), and each thread gets a pointer to it as a member of their struct parameter.

I also changed the definition of void resource(void* params) to void* resource(void *params) as that is what pthread_create expects as its third parameter.

Your wait() and signal() functions were able to be replaced one-to-one with pthread_mutex_lock() and pthread_mutex_unlock() from pthread.h which you have already included.

#include <stdio.h>
#include <pthread.h>

struct parameters{
    int thread_num;
    pthread_mutex_t *mutex; //mutex could be global if you prefer
};

void* resource(void *params){ //pthread_create expects a pointer to a function that takes a void* and returns a void*
    //assuming parameter is a parameters struct.
    struct parameters *p = (struct parameters*)params;
    pthread_mutex_lock(p->mutex); 
    printf("Resource is being used by thread %d\n", p->thread_num);
    pthread_mutex_unlock(p->mutex);
    return NULL;
}

int main(void){
    pthread_t threads[4];
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
    struct parameters ps[4]={{1, &mutex},{2, &mutex},{3, &mutex},{4, &mutex}};
    for(int counter = 0; counter < 4;   counter)
        pthread_create(&threads[counter], NULL, resource, &ps[counter]);
    //Threads should be joined
    for(int counter = 0; counter < 4;   counter)
        pthread_join(threads[counter], NULL);
}

This will eliminate the stochasticity that you are experiencing.

CodePudding user response:

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.

  • Related