c++mutexsemaphore

How is it possible that I'm locking a mutex multiple times, if mutex shall be possible to be locked only once?


I'm trying to understand c++ semaphore and mutex, and I found out that I'm locking 1 mutex several times, or at least my debugging messages are showing that is the case. Even though we have only 1 mutex in semaphore, I'm aquiring it for more than 1 thread. I understand that semaphore is supposed to allow the access by mutltiple threads (limited by the counter), but internally semaphore is made of mutex that can have value 0 or 1, taken or not taken. In my opinion it shall be not possible that 2 threads will lock the mutex 1 after another, but I can see that in the log file

/**
 * Connecting cell phones to a charger
 */
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <cstdio>
#include <bits/stdc++.h>

unsigned int data_race = 0;

class Semaphore {
public:
    Semaphore(unsigned long init_count) {
        count_ = init_count;
    }

    void acquire(int id) { // decrement the internal counter
        printf("%d wants to aquire.\n", id);
        std::unique_lock<std::mutex> lck(m_);
        printf("%d aquired mutex in aquire\n", id);
        while (!count_) {
            printf("%d is waiting for lock\n", id);
            cv_.wait(lck);
        }
        printf("%d is about to decrease count\n", id);
        count_--;
    }

    void release(int id) { // increment the internal counter
        printf("%d is about to relase \n", id);
        std::unique_lock<std::mutex> lck(m_);
        printf("%d acuired mutex in release\n", id);
        count_++;
        lck.unlock();
       cv_.notify_one();
    }

private:
    std::mutex m_;
    std::condition_variable cv_;
    unsigned long count_;
};

Semaphore charger(2);

void cell_phone(int id) {
    charger.acquire(id);
    printf("Phone %d is charging...\n", id);
    srand(id); // charge for "random" amount between 1-3 seconds
    std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 2000 + 1000));
    printf("Phone %d is DONE charging!\n", id);
    charger.release(id);
}

int main() {
    std::thread phones[6];
    for (int i=0; i<6; i++) {
        phones[i] = std::thread(cell_phone, i);
    }
    for (auto& p : phones) {
        p.join();
    }
}

It's compiled with c++17, I thought that only 1 mutex will be aquired at once in this implemenation.

The result:

PS C:\HHardDrive\embedded C\output> & .\'Semaphore_CriticalSection2.exe'
0 wants to aquire.
0 aquired mutex in aquire    //->Mutex locked first time
0 is about to decrease count
Phone 0 is charging...      
4 wants to aquire.
4 aquired mutex in aquire    //->Mutex locked second time
4 is about to decrease count
Phone 4 is charging...      
3 wants to aquire.
3 aquired mutex in aquire   
3 is waiting for lock       
2 wants to aquire.
2 aquired mutex in aquire   
2 is waiting for lock       
5 wants to aquire.
5 aquired mutex in aquire
1 wants to aquire.
5 is waiting for lock
1 aquired mutex in aquire
1 is waiting for lock
Phone 0 is DONE charging!
0 is about to relase 
0 acuired mutex in release
Phone 4 is DONE charging!
3 is about to decrease count
4 is about to relase
4 acuired mutex in release
Phone 3 is charging...
2 is about to decrease count
Phone 2 is charging...
Phone 2 is DONE charging!
2 is about to relase 
Phone 3 is DONE charging!
2 acuired mutex in release
3 is about to relase
5 is about to decrease count
Phone 5 is charging...
3 acuired mutex in release
1 is about to decrease count
Phone 1 is charging...
Phone 1 is DONE charging!
Phone 5 is DONE charging!
1 is about to relase
5 is about to relase 
1 acuired mutex in release
5 acuired mutex in release

Solution

  • Thread 0 unlocked the mutex in acquire() in the destructor of std::unique_lock<std::mutex> lck, which executed upon exiting the block where it was defined. This happened just before acquire returned, and shortly after the message "0 is about to decrease count" was printed. So thread 0 had indeed unlocked the lock before thread 4 locked it.

    This RAII pattern is the whole point of unique_lock; it locks the mutex at the point where the unique_lock is defined (i.e. where its constructor runs), holds the lock from there until the unique_lock goes out of scope (normally the end of the enclosing block), and then unlocks it in the destructor.