0

Suppose we have whole-part object relationship. For instance, there is a Boss that can own multiple Workers. For a Boss, we want the following operations:

  • add a new Worker to its "collection"
  • transfer an existing Worker object to another Boss object

I provide some C++ code that illustrates this idea below (some functions were skipped).

class Worker {
private:
    Boss *owner = nullptr;
    std::string name;

public:
    Worker(std::string name) : name(name), owner(nullptr) { }

    Boss* getOwner() const {
        return this->owner;
    }

    void setOwner(Boss *owner) {
        this->owner = owner;
    }
};

class Boss {
private:
    std::set<Worker *> workers;
    std::string name;

public:
    Boss(std::string name) : name(name) { }

    ~Boss() {
        for (auto worker : this->workers) {
            delete worker;
        }
        this->workers.clear();
    }

    void addWorker(Worker *worker) {
        this->workers.emplace(worker);
        worker->setOwner(this);
    }

    void transferOwnership(Worker *dummyWorker, Boss *newOwner);
};

int main()
{
    Boss b1{ "boss1" };
    Boss b2{ "boss2" };

    b1.addWorker(new Worker{ "w1" });
    b1.addWorker(new Worker{ "w2" });

    b2.addWorker(new Worker{ "w3" });

    Worker temp{ "w2" };
    b1.transferOwnership(&temp, &b2);

    b1.print(); // boss: boss1 having workers: w1
    b2.print(); // boss: boss2 having workers: w2 w3

    return 0;
}

A serial implementation of the transferOwnership() function would be the following:

void Boss::transferOwnership(Worker *dummyWorker, Boss *newOwner) {
    // find existing worker
    Worker *actualWorker = nullptr;
    for (auto worker : this->workers) {
        if (*worker == *dummyWorker) {
            actualWorker = worker;
        }
    }

    if (nullptr == actualWorker) {
        return;
    }

    // remove existing worker from this
    this->workers.erase(actualWorker);

    // add worker to other (newOwner)
    newOwner->workers.emplace(actualWorker);
    actualWorker->setOwner(newOwner);
}

Now, we want to parallelize this scenario. Suppose there are multiple threads that can execute any of the available operations (addWorker() or transferOwnership()) on any of the existing Boss objects.

The question is how to synchronize the code such that we always get a consistent result at the end and not get into deadlocks.

I think a solution would be to have a static mutex variable inside the Boss class, lock it at the beginning of the operation and end it at the end. This will protect all workers sets of all Boss objects.

Another solution would be to have a mutex for each Boss object. However, I think here we might encounter deadlocks as in transferOwnership() we will need to lock/unlock the mutexes of both Bosses and I think we cannot impose a certain locking order in this situation.

Can you think of other/better solutions?

Vlad C.
  • 3
  • 3

1 Answers1

0

The static mutex solution works. However, if you do want to have a mutex for each object, then your transaction requires locking of 2 bosses and 1 worker (depending on your requirements you may need to lock only the 2 boss objects).

In order to avoid deadlocks in this scenario, simply implement a function that locks the 3 mutexes according to some well-known pre-defined order. Their location in memory may serve for that purpose.

Your function would look like:

  1. sort the 2 or 3 mutexes according to their memory address
  2. lock them according to that order (make sure that you don't get the same mutex twice)

If you're on C++11 you can just use the std::lock(std::mutex... mutexes) that does more or less the same thing.

void lock(std::mutex& m1, std::mutex& m2) {
  if (&m1==&m2)
    m1.lock();
  else if (&m1<&m2) {
    m1.lock();
    m2.lock();
  } else {
    m2.lock();
    m1.lock();
  }
}

Unlocking is done in reverse order.

Shloim
  • 5,281
  • 21
  • 36