Suedocode
Suedocode

Reputation: 2524

Implementing a Semaphore with std::mutex

As a learning exercise, I'm just trying to make a Semaphore class using std::mutex and a few other things provided by the C++ standard. My semaphore should allow as many readLock() as needed, however a writeLock() can only be acquired after all reads are unlocked.

//Semaphore.h
#include <mutex>
#include <condition_variable>

class Semaphore{
public:
    Semaphore();
    void readLock(); //increments the internal counter
    void readUnlock(); //decrements the internal counter
    void writeLock(); //obtains sole ownership.  must wait for count==0 first
    void writeUnlock(); //releases sole ownership.
    int count; //public for debugging
private:
    std::mutex latch;
    std::unique_lock<std::mutex> lk;
    std::condition_variable cv;
};

//Semaphore.cpp
#include "Semaphore.h"
#include <condition_variable>
#include <iostream>

using namespace std;

Semaphore::Semaphore() : lk(latch,std::defer_lock) { count=0; }
void Semaphore::readLock(){ 
    latch.lock();
    ++count;
    latch.unlock();
    cv.notify_all(); //not sure if this needs to be here?
}
void Semaphore::readUnlock(){
    latch.lock();
    --count;
    latch.unlock();
    cv.notify_all(); //not sure if this needs to be here?
}
void Semaphore::writeLock(){
    cv.wait(lk,[this](){ return count==0; }); //why can't std::mutex be used here?
}
void Semaphore::writeUnlock(){ 
    lk.unlock();
    cv.notify_all();
}

My test program will writeLock() the semaphore, start a bunch of threads, and then release the semaphore. Immediately afterwards, the main thread will attempt to writeLock() the semaphore again. The idea is that when the semaphore becomes unlocked, the threads will readLock() it and prevent the main thread from doing anything until they all finish. When they all finish and release the semaphore, then the main thread can acquire access again. I realize this may not necessarily happen, but it's one of the cases I'm looking for.

//Main.cpp
#include <iostream>
#include <thread>
#include "Semaphore.h"

using namespace std;

Semaphore s;

void foo(int n){
    cout << "Thread Start" << endl;
    s.readLock();
    this_thread::sleep_for(chrono::seconds(n));
    cout << "Thread End" << endl;
    s.readUnlock();
}

int main(){
    std::srand(458279);
    cout << "App Launch" << endl;
    thread a(foo,rand()%10),b(foo,rand()%10),c(foo,rand()%10),d(foo,rand()%10);

    s.writeLock();
    cout << "Main has it" << endl;

    a.detach();
    b.detach();
    c.detach();
    d.detach();

    this_thread::sleep_for(chrono::seconds(2));
    cout << "Main released it" << endl;
    s.writeUnlock();

    s.writeLock();
    cout << "Main has it " << s.count << endl;
    this_thread::sleep_for(chrono::seconds(2));
    cout << "Main released it" << endl;
    s.writeUnlock();

    cout << "App End" << endl;

    system("pause"); //windows, sorry
    return 0;
}

The program throws an exception saying "unlock of unowned mutex". I think the error is in writeLock() or writeUnlock(), but I'm not sure. Can anyone point me in the right direction?

EDIT: There was a std::defer_lock missing when initializing lk in the constructor, however it didn't fix the error I was getting. As mentioned in the comment, this isn't a semaphore and I apologize for the confusion. To reiterate the problem, here is the output that I get (things in parenthesis are just my comments and not actually in the output):

App Launch
Thread Start
Thread Start
Main has it
Thread Start
Thread Start
Thread End (what?)
Main released it
f:\dd\vctools\crt_bld\self_x86\crt\src\thr\mutex.c(131): unlock of unowned mutex
Thread End
Thread End
Thread End

Upvotes: 4

Views: 7247

Answers (1)

Jonathan Wakely
Jonathan Wakely

Reputation: 171253

This is definitely not a "semaphore".

Your Semaphore constructor acquires the lock on latch right away, then you unlock it twice because writeUnlock() calls lk.unlock() and the next call to writeLock() tries to wait on a condition variable with an unlocked mutex, which is undefined behaviour, then you the next call to writeUnlock() tries to unlock an unlocked mutex, which is also undefined behaviour.

Are you sure the constructor should lock the mutex right away? I think you want to use std::defer_lock in the constructor, and then lock the mutex in writeLock().

Upvotes: 3

Related Questions