Reputation: 393
In the book "Concurrency in Action", there's an implementation of thread-safe stack where mutex is acquired/locked upon entering pop() and empty() functions like shown below:
class threadsafe_stack {
private:
std::stack<T> data;
mutable std::mutex m;
public:
//...
void pop(T& value) {
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
value = std::move(data.top());
data.pop();
}
bool empty() const {
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};
My question is, how does this code not run into a deadlock, when a thread, that has acquired the lock upon entering pop() is calling empty() which is protected by mutex as well? If lock() is called by the thread that already owns the mutex, isn't that undefined behavior?
Upvotes: 4
Views: 192
Reputation: 10460
how does this code not run into a deadlock, when a thread, that has acquired the lock upon entering pop() is calling empty() which is protected by mutex as well?
Because you are not calling empty
member function of threadsafe_stack
but you are calling empty() of class std::stack<T>
. If the code would be:
void pop(T& value)
{
std::lock_guard<std::mutex> lock(m);
if(empty()) // instead of data.empty()
throw empty_stack();
value = std::move(data.top());
data.pop();
}
Then, it would be undefined behavior:
If lock is called by a thread that already owns the mutex, the behavior is undefined: for example, the program may deadlock. An implementation that can detect the invalid usage is encouraged to throw a std::system_error with error condition resource_deadlock_would_occur instead of deadlocking.
Learn about recursive and shared mutex.
Upvotes: 2
Reputation: 123431
You would be correct if pop
would call this->empty
. Locking the same mutex twice via a std::lock_guard
is undefined behavior unless the locked mutex is a recursive one.
From cppreference on the constructor (the one that is used in the example code):
Effectively calls m.lock(). The behavior is undefined if m is not a recursive mutex and the current thread already owns m.
For the sake of completeness, there is a second constructor:
lock_guard( mutex_type& m, std::adopt_lock_t t );
which
Acquires ownership of the mutex m without attempting to lock it. The behavior is undefined if the current thread does not own m.
However, pop
calls data.empty
and this is the method of the private member, not the member function empty
of threadsafe_stack
. There is no problem in the code.
Upvotes: 1
Reputation: 11370
Not 100% sure what you mean, I guess you mean calling pop
and empty
sequentially in the same thread? Like in
while(!x.empty()) x.pop();
std::lock_guard
follows RAII. This means the constructor
std::lock_guard<std::mutex> lock(m);
will acquire/lock the mutex and the destructor (when lock
goes out of scope) will release/unlock the mutex again. So it's unlocked at the next function call.
Inside pop
only data.empty()
is called, which is not protected by a mutex. Calling this->empty()
inside pop
would indeed result in undefined behaviour.
Upvotes: 1