0

I want to implement semaphore class. And a user on stackoverflow has noted that my implementation is not working correct.

At the first I done it like this:

class sem_t {
    int count;
public:
    sem_t(int _count = 0) : count(_count) {};
    void up() {
        this->count++;
    }
    void down() {
        while (this->count == 0)
            std::this_thread::yield();
        this->count--;
    }
};

Then a user on stackoverflow noted that this implementation is faulty cause I read and write variable count out of any synchronization primitive and at some point the value can become incorrect and in case of compiler optimization compiler can assume that the variable count can not be modified by another thread. So, I tried to add mutex to this construction and I've done it like this:

class sem_t {
    int count;
    std::mutex mutualExclusion;
public:
    sem_t(int _count = 0) : count(_count) {};
    void up() {
        this->mutualExclusion.lock();
        this->count++;
        this->mutualExclusion.unlock();
    }
    void down() {
                this->mutualExclusion.lock();
        while (this->count == 0)
            std::this_thread::yield();
        this->count--;
        this->mutualExclusion.unlock();
    }
};

But in case of using this approach when I try to detach thread i got an error saying that mutex has been destroyed while busy, cause one thread can hold mutex and then yield after which the thread is detached and error occurs (Is this solution ok?). Then I tried to modify this code and I stoped on following construction:

class sem_t {
    int count;
    std::mutex mutualExclusion;
public:
    sem_t(int _count = 0) : count(_count) {};
    void up() {
        this->mutualExclusion.lock();
        this->count++;
        this->mutualExclusion.unlock();
    }
    void down() {
        while (this->count == 0)
            std::this_thread::yield();
        this->mutualExclusion.lock();
        this->count--;
        this->mutualExclusion.unlock();
    }
};

But I think that this solution is faulty too, cause it can lead the same problem as the first solution.

So, whats the correct implementation? I wanna note that I tried implementation with condition variable, but i am trying to implement semaphore without condition variable and if you want to suggest some solution with condition variable please describe how wait method of condition variable is working.

[Edit]

My full code, using self implemented semaphore:

#include "pch.h"
#include <iostream>
#include <vector>
#include <mutex>
#include <thread>
#include <chrono>

class sem_t {
    int count;
    std::mutex mutualExc;
public:
    sem_t(int _count = 0) : count(_count) {};
    void up() {
        mutualExc.lock();
        this->count++;
        mutualExc.unlock();
    }
    void down() {
        mutualExc.lock();
        while (this->count == 0) {
            mutualExc.unlock();
            std::this_thread::yield();
            mutualExc.lock();
        }
        this->count--;
        mutualExc.unlock();
    }
};

#define N 5
#define THINKING 0
#define HUNGRY 1
#define EATING 2

std::mutex mx;
std::mutex coutMX;
char philosopherState[N] = { THINKING };
sem_t philosopherSemaphores[N] = { 0 };

void testSetState(short i) {
    if (philosopherState[i] == HUNGRY && philosopherState[(i + 1) % N] != EATING && philosopherState[(i + N - 1) % N] != EATING) {
        philosopherState[i] = EATING;
        philosopherSemaphores[i].up();
    }
}

void take_forks(short i) {
    ::mx.lock();
    philosopherState[i] = HUNGRY;
    
    testSetState(i);
    ::mx.unlock();
    philosopherSemaphores[i].down();
}
void put_forks(short i) {
    ::mx.lock();
    philosopherState[i] = THINKING;

    testSetState((i + 1) % N);
    testSetState((i + N - 1) % N);
    ::mx.unlock();
}

void think(short p) {
    for (short i = 0; i < 5; i++) {
        coutMX.lock();
        std::cout << "Philosopher N" << p << " is thinking!" << std::endl;
        coutMX.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
}
void eat(short p) {
    for (short i = 0; i < 5; i++) {
        coutMX.lock();
        std::cout << "Philosopher N" << p << " is eating!" << std::endl;
        coutMX.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
}

void philosopher(short i) {
    while (1) {
        think(i);
        take_forks(i);
        eat(i);
        put_forks(i);
    }
}

int main()
{
    std::vector<std::thread*> threadsVector;
    for (int i = 0; i < N; i++) {
        threadsVector.push_back(new std::thread([i]() { philosopher(i); }));
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(15000));

    for (int i = 0; i < N; i++) {
        threadsVector[i]->detach();
    }

    return 0;
}

Error that is occurring (only occurs when running program in release or debug mode in visual studio)

This error only occurs while running program in visual studio

The console when error occurs

Community
  • 1
  • 1
h4ckthepl4net
  • 391
  • 1
  • 12
  • 1
    In real life programming, you want to use `condition_variable`, atomic increment and decrement, no global variable, proper mecanism for stopping threads etc. If you are serious about multithreading in C++, you **must read** the book **C++ Concurrency In Action**. – Phil1970 Jun 15 '19 at 13:52
  • @Phil1970 Ok, I will read that book as soon as possible, thanks for advice. – h4ckthepl4net Jun 15 '19 at 13:54
  • 1
    As the **Dinning philosopher problem** is mainly academic, usually it would be assume that it runs forever. – Phil1970 Jun 15 '19 at 13:54

1 Answers1

2

The last attempt is indeed not correct because it may happend that several threads call down at the same time and all successfully pass

    while (this->count == 0)
        std::this_thread::yield();

lines and then they will all decrement the counter to possiby negative value:

    this->mutualExclusion.lock();
    this->count--;
    this->mutualExclusion.unlock();

So, the counter value check and update must be performed atomically.

If you want to keep busy loop the easiest way would be just to call unlock before yield and lock after, so compare and decrement will be performed under the same lock:

void down() {
    this->mutualExclusion.lock();
    while (this->count == 0) {
        this->mutualExclusion.unlock();
        std::this_thread::yield();
        this->mutualExclusion.lock();
    }
    this->count--;
    this->mutualExclusion.unlock();
}

Also, you can use std::unique_lock guard which locks provided mutex in constructor and unlocks in destructor, so the mutex will not be accidentaly left in locked state:

void down() {
    std::unique_lock<std::mutex> lock(this->mutualExclusion);
    while (this->count == 0) {
        lock.unlock();
        std::this_thread::yield();
        lock.lock();
    }
    this->count--;
}

To deal with "muxed destroyed while busy" error you need either to have a flag to stop background threads and wait for them to complete with join instead of detach:

#include <atomic>

...

std::atomic<bool> stopped{ false };

void philosopher(short i) {
    while (!stopped) {
        ...
    }
}

...

int main()
{
    ...

    stopped = true;
    for (int i = 0; i < N; i++) {
        threadsVector[i]->join();
    }
    return 0;
}

or if you really don't want to care of releasing static resources you can call std::quick_exit instead of detach and return:

int main()
{
    ...
    std::this_thread::sleep_for(std::chrono::milliseconds(15000));
    std::quick_exit(0);
}
dewaffled
  • 2,850
  • 2
  • 17
  • 30
  • I tried that solution, but when detaching threads in some cases it leads to "mutex destroyed while busy" error too and that's what I'm confused of. – h4ckthepl4net Jun 14 '19 at 13:27
  • Is the condition variable working in the same principle? I can't understand why condition variable solution is not leading to the same error. – h4ckthepl4net Jun 14 '19 at 13:29
  • It seems that the solution without a notification mechanism suffers from the issues mentioned in [this post](https://stackoverflow.com/a/12569318/5376789). – xskxzr Jun 14 '19 at 13:37
  • @h4ckthepl4net Are you sure you are just detaching the thread and not destroying semaphor instance while it is in blocked state used in other thread? Can you add minimal example reproducing the issue when "detaching"? – dewaffled Jun 14 '19 at 14:33
  • @dewaffled Yes, of course. I will add example to my question. – h4ckthepl4net Jun 14 '19 at 14:42
  • 1
    @h4ckthepl4net So, you have global mutexes and semaphors and returning from main while they are used by other threads - this is not correct. I added a few termination ideas examples to the answer. Hope this helps. – dewaffled Jun 14 '19 at 17:20
  • @dewaffled Thanks for that answer, now i can be sure that the problem is not in implementation of my semaphore. Sorry for wasting your time :) – h4ckthepl4net Jun 14 '19 at 21:14