-2

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?

  • 3
    Access to your `binary_semaphore` needs to be *atomic* and use other synchronization primitives to lock access to it. Right now you'll have data-races and undefined behavior. – Some programmer dude Oct 05 '22 at 19:14
  • 2
    As a prime example: What if two threads wait for the semaphore. A third thread signals it and the first waiting thread exits the loop. But before it can mark it as in use, it's preempted by the second waiting thread. The second waiting thread exits the loop and marks it as in use. When the first waiting thread starts again it will *also* mark it as in use. Now you have *two* threads which thinks it's okay to continue, when it's not. The more threads you have, the worse this issue will be. – Some programmer dude Oct 05 '22 at 19:17
  • 4
    In short: Don't create your own threading primitives, use the ones that already exists. – Some programmer dude Oct 05 '22 at 19:18
  • 1
    On a different note, the `register` specifier really have no practical meaning these days, except that you can't take the address of such variable with the pointer-to `&` operator. The compiler is a much better arbiter about what should go into a register or not than anyone of us will ever be. Also, that loop would be much easier to read and understand by using a plain `for` loop: `for (unsigned counter = 0; counter < 4; ++counter) { ... }` – Some programmer dude Oct 05 '22 at 19:29
  • 1
    The immediate **problem** is your "clever" **while loop**. Stop being clever and use `for`. There is a good reason why it exists. If you don't believe, just print the values of `i` inside the loop. – Antti Haapala -- Слава Україні Oct 05 '22 at 19:30
  • 1
    Also note that *all* kind of pointers (except function pointers) can be implicitly converted to `void *`. You never need to do an explicit cast to (or from) `void *` for pointers. And you *really* should make it a habit to add error checking. `pthread_create` can fail. And don't forget to actually *join* the threads once you're done with them. And remember that pthread thread-functions are supposed to *return* a value (well, a pointer really). – Some programmer dude Oct 05 '22 at 19:31
  • To summarize: I think you need to take a few steps back, and study more about threads in general, and the POSIX thread API. – Some programmer dude Oct 05 '22 at 19:34
  • 1
    Synchronization libraries are written specifically for the operating systems that they work on, so they are able to account for the ways that the operating system schedules processes and when they yield etc to actually ensure that there aren't race conditions. The big picture here is that you can't just name an integer "semaphore" and expect it to actually provide some sort of concurrency protection. Try using a mutex from a real concurrency library, such as `mtx_t` from StdC `thread.h` or `pthread_mutex_t` on a UNIX-based platform – Willis Hershey Oct 05 '22 at 19:50
  • @AnttiHaapala--СлаваУкраїні Sorry daddy – FariyaAchhab Oct 06 '22 at 10:19
  • As a separate matter, `signal()` is the name of a C standard library function, and `wait()` is the name of standard POSIX function. It is unwise to give your own functions the same names as those. – John Bollinger Oct 06 '22 at 19:23

2 Answers2

0

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 a prototype that matches 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.

Willis Hershey
  • 1,520
  • 4
  • 22
0

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.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157