-1

I'm trying to solve the following problem: " The analogy is based upon a hypothetical barber shop with one barber. The barber has one barber's chair in a cutting room and a waiting room containing a number of chairs in it. When the barber finishes cutting a customer's hair, he dismisses the customer and goes to the waiting room to see if there are others waiting. If there are, he brings one of them back to the chair and cuts their hair. If there are none, he returns to the chair and sleeps in it.

Each customer, when they arrive, looks to see what the barber is doing. If the barber is sleeping, the customer wakes him up and sits in the cutting room chair. If the barber is cutting hair, the customer stays in the waiting room. If there is a free chair in the waiting room, the customer sits in it and waits their turn. If there is no free chair, the customer leaves. "

My entire code is the following:

// Example program
#include <iostream>
#include <string>
#include <mutex>
#include <thread>
#include <chrono>
#include <atomic>
#include <deque>
#include <vector>
#include <condition_variable>
using namespace std;

const unsigned int NUM_CHAIRS = 2;
const unsigned int NUM_CUSTOMERS = 1;

// In a barbershop, customers are coming in and there is only one barber. If the barber is cutting a
// customers hair, the customer will wait. There are only a limited amount of chairs for the customer
// to wait on. If the chairs are full, then the customer will leave and try again later.
    
struct Barbershop
{
    public:
        Barbershop(): num_waiting(0){};
        condition_variable cv; // Acts like a receptionist
        atomic<int> num_waiting; // Number of customers waiting on chairs
        mutex cout_mtx;

};


class Barber
{
    public:
        Barber(Barbershop& shop): mem_shop(shop), hislife(&Barber::work, this){};
        void work()
        {
            unique_lock<mutex> lk(b_mu);
            if(mem_shop.num_waiting == 0)
            {
                mem_shop.cout_mtx.lock();
                cout << "Barber sleeping...\n";
                mem_shop.cout_mtx.unlock();
            }
            mem_shop.cv.wait(lk, [this]{return mem_shop.num_waiting > 0;}); // []{return mem_shop.num_waiting}
            mem_shop.cout_mtx.lock();
            cout << "Time to cut some hair!\n";
            mem_shop.cout_mtx.unlock();
            lk.unlock();
            mem_shop.cv.notify_one();
            this_thread::sleep_for(chrono::milliseconds(100));
            mem_shop.num_waiting--;
        };
        ~Barber()
        {
            hislife.join();
        }
        int jn;
        mutex b_mu;
        Barbershop& mem_shop;
        thread hislife;
        
};

class Customer
{
    public:
        Customer(Barbershop& shop, int name_id): mem_shop(shop), name(name_id), hislife(&Customer::tryGetHaircut, this){};
        void tryGetHaircut()
        {
            if(mem_shop.num_waiting == NUM_CHAIRS)
            {
                mem_shop.cout_mtx.lock();
                cout << name <<"'s wait was too long. Leaving!\n";
                mem_shop.cout_mtx.unlock();
            }
            else
            {
                mem_shop.num_waiting++;
                std::unique_lock<mutex> lk(c_mu);
                mem_shop.cv.wait(lk);
                mem_shop.cout_mtx.lock();
                cout << name << " is getting haircut!\n";
                mem_shop.cout_mtx.unlock();
            }
        };
        ~Customer()
        {
            hislife.join();
        }
        mutex c_mu;
        Barbershop& mem_shop;
        thread hislife;
        unsigned int name;

};

int main()
{
    Barbershop mybarbershop;
    Barber dan();
    vector<Customer*> customers;
    customers.reserve(NUM_CUSTOMERS-1);
    for(unsigned int i=0; i<NUM_CUSTOMERS; i++)
    {
        customers.push_back(new Customer(mybarbershop, i+1));
    }

    for(auto s : customers)
    {
        delete s;
    }
    cout << "Program Finished!\n";
    return 0;
}

However, my code isn't even making it into the Barbers thread and seems to get locked on the condition variables. Are there any blatant issues I've made in logic/syntax in solving this problem?

I am still a beginner so any help on the topic would be greatly appreciated.

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
Shizo
  • 5
  • 3
  • OT: `std::vector> customers;` might be a better option. – Daniel Langr Dec 02 '21 at 12:13
  • You also don't need `cout_mtx` at all. Writing to `std::cout` is guaranteed to be _thread-safe_ by itself. See, e.g., https://stackoverflow.com/q/6374264/580083. Acquiring a mutex under another locked mutex smells with a deadlock. – Daniel Langr Dec 02 '21 at 12:18
  • @DanielLangr it is thread safe but you may get lines mixed due to buffers being flushed simultaneously. – Jorge Bellon Dec 02 '21 at 12:24
  • @DanielLangr thanks for the responses! I'll read the link and try it out. Did you happen to try run the code by any chance as well? – Shizo Dec 02 '21 at 12:31
  • @JorgeBellon Even with the mutex-protected writes to `std::cout` there is no guarantee that the output won't get mixed. – Daniel Langr Dec 02 '21 at 12:34
  • @DanielLangr true, not in this case because there's no `std::endl` nor a flush. – Jorge Bellon Dec 02 '21 at 12:38
  • 2
    @JorgeBellon AFAIK, even `endl` or `flush` resolves flushing only at a library level. OS still can use some internal buffers, which, generally, cannot be controlled from a user space. – Daniel Langr Dec 02 '21 at 12:40

1 Answers1

0

You have a few problems:

  1. There's a data race beteween if(mem_shop.num_waiting == NUM_CHAIRS) and mem_shop.num_waiting++ are not protected under a lock. Therefore two customers can join the barbershop at the same time, see a chair and both using the same chair. Hence it is possible that num_waiting > NUM_CHAIRS and more clients will join the queue when it is too long. You need to either use an atomic compare and swap or just protect the variable access with a lock.

  2. mem_shop.cv.wait(lk, [this]{return mem_shop.num_waiting > 0;}); makes the barber to wait if there is people waiting. Shouldn't the condition be the opposite (wait if no one's waiting)? [this]{return !mem_shop.num_waiting;}. Also there's a data race accessing num_waiting because the barber and the clients are not accessing the same lock. Either turn num_waiting into atomic or make the barber and clients share the mutex through barbershop (so accessing both count and conditional variable is protected using the same mutex).

Note: the fact that num_waiting is atomic, does not make every operation atomic. If you do two atomic operations they won't be atomic as a whole, so if (waiting == MAX) is reading a value, then taking a decission, then waiting++ is updating the value. Using a compare and swap here may be a solution to do both things in one atomic access:

atomic_uint waiting;
unsigned expected = waiting.load();
do {
  desired = expected + 1;
} while (desired < MAX_WAITING &&
         waiting.compare_exchange_weak(expected, desired));

Here, compare_exchange_weak checks that the value in memory matches desired and returns true if that's the case. Otherwise it will return false and update expected with the last value. In that case you just need to recheck that the value you want to store still meets the boundaries (< MAX) and if it does not then you just quit.

Jorge Bellon
  • 2,901
  • 15
  • 25
  • Thanks so much for your response! In regards to: 1. Would there be a data race? I've set num_waiting as an atomic so by my understanding, there shouldn't be any data races on this value, no? 2. Thanks. I must have mistaken the documentation, thanks for the pick up, – Shizo Dec 02 '21 at 12:35
  • The atomic operations guarantees that any two threads doing a single operation on an atomic are guaranteed to get a deterministic result. For example, if two threads do `numWaiting++`, then eventually the value in memory will increase by 2. However, you are doing 2 different atomic operations, which by definition is not atomic: first you are loading the value to compute the conditional, then you are performing an increment. – Jorge Bellon Dec 02 '21 at 12:44
  • It's much easier to understand if you put a `std::this_thread::sleep_for(std::chrono::seconds(2))` before `mem_shop.num_waiting++`. You can even use a debugger to see its effect. – Jorge Bellon Dec 02 '21 at 12:46