Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 8

Exercise 7.1. Fig. 7.

33 shows an alternative implementation of CLHLock in which a thread reuses its


own node instead of its predecessor node. Explain how this implementation can go wrong, and how
the MCS lock avoids the problem even though it reuses thread-local nodes.

Solution:

In this implementation, each thread reuses its own node for locking and unlocking instead of
maintaining a separate predecessor node. This issue can lead to a deadlock situation for a thread that
attempts to reacquire the lock.

Deadlock Scenario:

Thread A initially acquires the lock, and the same node is referenced by both tail and myNode (since tail
was set to this node during the lock acquisition).

Thread A releases the lock, setting the locked field to false.

Later, thread A attempts to reacquire the lock. It sets the node's locked field to true (Line 8) and swaps
that node with itself (Line 10). This means that thread A now considers itself as its own predecessor.

Thread A enters a spin loop while waiting for its own node's locked field to become false (Line 13).
However, it is not possible for the lockedfield of its own node to becomefalse because thread A itself set
it to true.

As a result, thread A becomes deadlocked, as it waits for its own node to unlock, which will never
happen. This leads to a situation where the lock acquisition cannot proceed, and thread A is effectively
stuck.

The MCS lock maintains a well-defined queue structure to avoid this scenario. MCS lock avoids the self-
deadlock problem by clearly distinguishing between a thread's node and its predecessor's node in the
queue and threads spin while waiting for their predecessors to release the lock, not for their own nodes
to unlock. This design ensures that threads don't contend with themselves and allows for efficient and
deadlock-free lock acquisition and release in a concurrent environment.

Exercise 8.6. In the shared-bathroom problem, there are two classes of threads, called MALE and
FEMALE. There is a single Bathroom resource that must be used in the following way:

1. Mutual exclusion: persons of opposite sex may not occupy the bathroom simultaneously.

2. Weak starvation-freedom: Assuming that eventually there will be both a male and a female who
want to use the bathroom, then everyone who needs to use the bathroom eventually enters.

The protocol specifies the following four procedures: enterMale() delays the caller until a male can
enter the bathroom, and leaveMale() is called when a male leaves the bathroom, while enterFemale()
and leaveFemale() do the same for females.
For example,

enterMale();

teeth.brush(toothpaste);

leaveMale();

Implement this class using locks and condition variables. Explain why your implementation satisfies
mutual exclusion and weak starvation-freedom.

Solution:

#include <iostream>

#include <mutex>

#include <condition_variable>

class Bathroom {

public:

Bathroom() : maleCount(0), femaleCount(0) {}

void enterMale() {

std::unique_lock<std::mutex> lock(mutex);

// Wait until there are no females in the bathroom

maleQueue.wait(lock, [this] { return femaleCount == 0; });

// A male can now enter the bathroom

maleCount++;

void leaveMale() {

std::unique_lock<std::mutex> lock(mutex);

maleCount--;

// If there are no males left, signal waiting females

if (maleCount == 0) {

femaleQueue.notify_all();

}
void enterFemale() {

std::unique_lock<std::mutex> lock(mutex);

// Wait until there are no males in the bathroom

femaleQueue.wait(lock, [this] { return maleCount == 0; });

// A female can now enter the bathroom

femaleCount++;

void leaveFemale() {

std::unique_lock<std::mutex> lock(mutex);

femaleCount--;

// If there are no females left, signal waiting males

if (femaleCount == 0) {

maleQueue.notify_all();

private:

int maleCount;

int femaleCount;

std::mutex mutex;

std::condition_variable maleQueue;

std::condition_variable femaleQueue;

};

int main() {

Bathroom bathroom;

// Example usage

bathroom.enterMale();

std::cout << "Male is using the bathroom\n";

// Perform bathroom activities

bathroom.leaveMale();
bathroom.enterFemale();

std::cout << "Female is using the bathroom\n";

// Perform bathroom activities

bathroom.leaveFemale();

return 0;

This implementation satisfies mutual exclusion and weak starvation-freedom:

1. Mutual Exclusion:

The mutual exclusion property ensures that persons of the opposite sex cannot occupy the bathroom
simultaneously. In this implementation:

- `enterMale()` and `enterFemale()` both use `std::unique_lock` to acquire the mutex before entering
the critical section. This ensures that only one male or female can access the bathroom's shared data
and update the counts at a time.

- In `enterMale()`, a male thread waits in the `maleQueue` if there are females in the bathroom. It only
proceeds when the `femaleCount` is zero, meaning there are no females in the bathroom. This
effectively prevents males from entering when females are present, ensuring mutual exclusion.

- Similarly, in `enterFemale()`, a female thread waits in the `femaleQueue` if there are males in the
bathroom. It only proceeds when the `maleCount` is zero, ensuring that females do not enter when
males are present.

This mechanism guarantees that, at any given time, either males or females can use the bathroom, but
not both. This is the essence of mutual exclusion.

2. Weak Starvation-Freedom:

Weak starvation-freedom ensures that if there are both males and females who want to use the
bathroom, they will eventually enter, and no one will be indefinitely starved. In this implementation:

- When a male wants to enter the bathroom using `enterMale()`, they will wait in the `maleQueue` if
there are females in the bathroom. However, if there are no females waiting to enter, the male can
immediately enter, ensuring that males are not starved indefinitely.

- Similarly, when a female wants to enter the bathroom using `enterFemale()`, they will wait in the
`femaleQueue` if there are males in the bathroom. But if there are no males waiting to enter, the female
can immediately enter.
- When a male leaves the bathroom using `leaveMale()`, they signal waiting females in the
`femaleQueue` if there are no more males in the bathroom. This allows females to enter if they are
waiting.

- When a female leaves the bathroom using `leaveFemale()`, they signal waiting males in the
`maleQueue` if there are no more females in the bathroom, allowing males to enter if they are waiting.

This signaling mechanism ensures that if there is a male and a female who want to use the bathroom,
they will eventually enter, as the leaving thread signals the opposite sex to enter. This guarantees weak
starvation-freedom, meaning that no one is indefinitely starved, and everyone who needs to use the
bathroom eventually enters.

3. Develop a abortable lock based on MCS lock. In other words, each thread should spin on its local
memory location and not on a memory location belonging to another processor/thread (that could
potentially be far away).

#include <iostream>

#include <atomic>

#include <thread>

#include <condition_variable>

#include <mutex>

#include <chrono>

#include <stdexcept>

class AbortableMCSLock {

public:

AbortableMCSLock() : tail(nullptr), abortLock(), isAborted(false) {

void lock() {

QNode* qnode = myNode();

isAborted = false;

QNode* pred = tail.exchange(qnode, std::memory_order_acquire);

if (pred != nullptr) {

qnode->locked = true;

pred->next.store(qnode, std::memory_order_relaxed);
while (!qnode->locked) {

if (isAborted) {

releaseResources(qnode);

throw AbortedException();

void unlock() {

QNode* qnode = myNode();

if (qnode->next.load(std::memory_order_relaxed) == nullptr) {

if (tail.compare_exchange_strong(qnode, nullptr, std::memory_order_release)) {

return;

while (qnode->next.load(std::memory_order_relaxed) == nullptr) {

if (isAborted) {

releaseResources(qnode);

throw AbortedException();

qnode->next.load(std::memory_order_relaxed)->locked = false;

qnode->next.store(nullptr, std::memory_order_relaxed);

void abort() {

isAborted = true;

abortCond.notify_one();

}
void awaitAbort() {

std::unique_lock<std::mutex> lock(abortMutex);

abortCond.wait(lock, [this] { return isAborted; });

bool awaitAbort(std::chrono::milliseconds timeout) {

std::unique_lock<std::mutex> lock(abortMutex);

return abortCond.wait_for(lock, timeout, [this] { return isAborted; });

private:

struct QNode {

bool locked = false;

std::atomic<QNode*> next{nullptr};

};

std::atomic<QNode*> tail;

std::mutex abortMutex;

std::condition_variable abortCond;

std::atomic<bool> isAborted;

QNode* myNode() {

static thread_local QNode node;

return &node;

void releaseResources(QNode* qnode) {

// Release any resources acquired during the lock waiting

// Add custom resource cleanup code as needed.

public:

class AbortedException : public std::runtime_error {

public:

AbortedException() : std::runtime_error("Lock acquisition was aborted") {


}

};

};

int main() {

AbortableMCSLock lock;

std::thread thread1([&lock]() {

try {

lock.lock();

// Simulate some work

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

lock.unlock();

catch (const AbortableMCSLock::AbortedException& e) {

std::cerr << "Thread 1 aborted" << std::endl;

});

std::thread thread2([&lock]() {

lock.lock();

lock.abort();

lock.unlock();

});

thread1.join();

thread2.join();

return 0;

You might also like