Why Is Locking a Std::Mutex Twice 'Undefined Behaviour'

Why is locking a std::mutex twice 'Undefined Behaviour'?

Because it never happens in a correct program, and making a check for something that never happens is wasteful (and to make that check it needs to store the owning thread ID, which is also wasteful).

Note that it being undefined allows debug implementations to throw an exception, for example, while still allowing release implementations to be as efficient as possible.

Why can a mutex lock twice without unlock in C++?

It cannot. Your code has undefined behavior.

From cppreference:

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.

Reading on, one might get the impression that calling lock on the same thread would trigger an exception always:

Exceptions

Throws std::system_error when errors occur, including errors from the
underlying operating system that would prevent lock from meeting its
specifications. The mutex is not locked in the case of any exception
being thrown.

However, there is not necessarily an exception when you call lock in the same thread. Only if the implementation can detect such invalid use and only if the implementation is kind enough to actually throw an exception.

Looking into the standard we find:

1 The class mutex provides a non-recursive mutex with exclusive
ownership semantics. If one thread owns a mutex object, attempts by
another thread to acquire ownership of that object will fail (for
try_­lock()) or block (for lock()) until the owning thread has
released ownership with a call to unlock().

2 [Note: After a thread A has called unlock(), releasing a mutex, it is possible for another thread B to lock the same mutex, observe that
it is no longer in use, unlock it, and destroy it, before thread A
appears to have returned from its unlock call. Implementations are
required to handle such scenarios correctly, as long as thread A
doesn't access the mutex after the unlock call returns. These cases
typically occur when a reference-counted object contains a mutex that
is used to protect the reference count. — end note ]

3 The class mutex meets all of the mutex requirements ([thread.mutex.requirements]). It is a standard-layout class
([class.prop]).

4 [Note: A program can deadlock if the thread that owns a mutex object calls lock() on that object. If the implementation can detect the
deadlock, a resource_­deadlock_­would_­occur error condition might be
observed. — end note ]

5 The behavior of a program is undefined if it destroys a mutex object owned by any thread or a thread terminates while owning a mutex
object.

It only says "can deadlock" and "might be observed" but otherwise it does not define what happens when you call lock in the same thread.

PS Requiring to throw an excpetion in this case would make calling lock more expensive without any real benefit. Calling lock twice in the same thread is something that you should just not do. Actually you shouldn't call std::mutex::lock directly at all, because manually releasing the lock is not exception safe:

// shared block - bad
{
mut.lock();
// ... code ...
mut.unlock();
}

If ...code.. throws an exception then mut is never unlocked. The way to solve that is RAII and fortunately the standard library offers lots of RAII-helpers for locks, eg std::lock_guard:

// shared block - good
{
std::lock_guard<std::mutex> lock(mut);
// ... code ...
}

TL;DR Don't do it.

If your code relies on that exception then you have more severe problems than not getting that exception.

Why does this double mutex lock not cause deadlock?

For a deadlock, you need at least two

By definition, a deadlock involves at least 2 parties. This was laid down by many authors, among others Hoare in his pioneering work Communicating Sequential Processes. This is also reminded in the C++ standard definitions (emphasis is mine):

17.3.8: Deadlock: one or more threads are unable to continue execution because
each is blocked waiting for one or more of the others to satisfy some
condition

A more illustrative definition is given by Anthony Williams, in C++ concurrency in action

Neither thread can proceed, because each is waiting for the other to release it's mutex. This scenario is called deadlock and it's the biggest problem with having to lock two or more mutexes.

You can therefore by definition not create a deadlock with a single thread in a single process.

Don't misunderstand the standard

The standard says on mutexes:

30.4.1.2.1/4 [Note: A program may deadlock if the thread that owns a mutex object calls lock() on that object.]

This is a non-normative note. I think it embarrassingly contradicts the standard's own definition. From the terminology point of view, a process that locks itself is in a blocked state.

But more important, and beyond the issue of deadlock terminology, the word "MAY" allows the said behavior for C++ implementations (e.g. if it is not able on a particular OS to detect a redundant lock acquisition). But it's not required at all : I believe that most mainstream C++ implementation will work fine, exactly as you have experienced yourself.

Want to experience with deadlocks ?

If you want to experience with real deadlocks, or if you want simply to find out if your C++ implementation is able to detect the resource_deadlock_would_occur error, here a short example. It could go fine but has high probability of creating a deadlock:

std::mutex m1,m2;
void foo() {
m1.lock();
std::cout<<"foo locked m1"<<std::endl;
std::this_thread::sleep_for (std::chrono::seconds(1));
m2.lock();
m1.unlock();
std::cout<<"foo locked m2 and releases m1"<<std::endl;
m2.unlock();
std::cout<<"foo is ok"<<std::endl;
}
void bar() {
m2.lock();
std::cout<<"bar locked m2"<<std::endl;
std::this_thread::sleep_for (std::chrono::seconds(1));
m1.lock();
m2.unlock();
std::cout<<"barlocked m1 and releases m2"<<std::endl;
m1.unlock();
std::cout<<"bar is ok"<<std::endl;
}
int main()
{
std::thread t1(foo);
bar();
t1.join();
std::cout << "Everything went fine"<<std::endl;
return 0;
}

Online demo

This kind of deadlock is avoided by locking the different mutexes always in the same order.

Can another tread unlock a mutex although it has not acquired the lock previously?

The link you shared already has the answer

The mutex must be locked by the current thread of execution, otherwise, the behavior is undefined.

"Undefined behavior" in C++ doesn't mean "the program can define it". It literally means "this behavior will never be defined by any conforming compiler". See this question for more details on the terminology.

Specifically, the standard (§32.5.4.2.1) has this to say

The expression m.unlock() is well-formed and has the following semantics:

Preconditions: The calling thread owns the mutex.

And §16.3.2.4 defines a function's preconditions as

Preconditions: the conditions that the function assumes to hold whenever it is called; violation of any preconditions results in undefined behavior.

So unlocking a mutex you don't own in undefined behavior. It could work, it could throw an exception, it could ignore the call, it could summon demons out your nose.

Scoped_lock with repeating arguments

The phenomena you would really afraid of is "self deadlock". It occurs, when 2 conditions are fulfilled simultaneously:

  • the same thread calls repeatedly mutex.lock() on the same mutex object,
  • and that mutex belongs to non-recursive type, which does not support such recursive locking.

In C++ you have 2 types of mutexes:

  • std::mutex is non-recursive type - it is quicker and requires less resources,
  • std::recursive_mutex is recursive type - it is safer but requires more resources.

Now let's apply this knowledge to your specific example:

  1. In your case, the obvious way to avoid "self deadlock" is to avoid the "self assignment". For this purpose simply add the additional check:

     S & operator = ( const S & b ) 
    {
    if (this != &b) // check it is not self assignment
    {
    std::scoped_lock l( m, b.m );
    v = b.v;
    }
    return * this;
    }
  2. If this is the only usage of your mutex, you can be sure that recursive "self deadlocking" will never occur. So, this is the best and cheapest solution.

  3. If you are going to add other "synchronized" method which calls the current one when mutex is already held by calling thread, then (and only in such case) you really need to replace the std::mutex type to be std::recursive_mutex.

Why doesn't mutex work without lock guard?

Suppose you have a room with two entries. One entry has a door the other not. The room is called shared_var. There are two guys that want to enter the room, they are called task_1 and task_2.

You now want to make sure somehow that only one of them is inside the room at any time.

taks_2 can enter the room freely through the entry without a door. task_1 uses the door called shared_mutex.

Your question is now: Can achieve that only one guy is in the room by adding a lock to the door at the first entry?

Obviously no, because the second door can still be entered and left without you having any control over it.

If you experiment you might observe that without the lock it happens that you find both guys in the room while after adding the lock you don't find both guys in the room. Though this is pure luck (bad luck actually, because it makes you beleive that the lock helped). In fact the lock did not change much. The guy called task_2 can still enter the room while the other guy is inside.

The solution would be to make both go through the same door. They lock the door when going inside and unlock it when leaving the room. Putting an automatic lock on the door can be nice, because then the guys cannot forget to unlock the door when they leave.

Oh sorry, i got lost in telling a story.

TL;DR: In your code it does not matter if you use the lock or not. Actually also the mutex in your code is useless, because only one thread un/locks it. To use the mutex properly, both threads need to lock it before reading/writing shared memory.



Related Topics



Leave a reply



Submit