ChatGPT解决这个技术问题 Extra ChatGPT

When to use recursive mutex?

I understand recursive mutex allows mutex to be locked more than once without getting to a deadlock and should be unlocked the same number of times. But in what specific situations do you need to use a recursive mutex? I'm looking for design/code-level situations.

Not actually a dupe, but overlapping: stackoverflow.com/questions/187761/…. That question asks "why on earth would anyone want to use a non-recursive mutex". This question asks "why on earth would anyone want to use a recursive mutex?". It's like alien civilisations colliding ;-)
@Steve: Yes, I did check that thread before but I just didn't get the answer I was looking for... I'm actually looking for specific designs where it is really needed..
I'm pretty sure there are none, or at least none simple and obviously-good-design enough to serve as killer apps. Any function which takes a mutex can be replaced by two functions, one which takes it and one which doesn't. Any function which wants to call such a function can call the appropriate one according to whether the mutex in question is already held. Any design in which you call functions without knowing whether or not you already hold a mutex which is relevant to that function, is probably broken. But look at almost any Java to see code which gains brevity from recursive locking.

A
Antti Huima

For example when you have function that calls it recursively, and you want to get synchronized access to it:

void foo() {
   ... mutex_acquire();
   ... foo();
   ... mutex_release();
}

without a recursive mutex you would have to create an "entry point" function first, and this becomes cumbersome when you have a set of functions that are mutually recursive. Without recursive mutex:

void foo_entry() {
   mutex_acquire(); foo(); mutex_release(); }

void foo() { ... foo(); ... }

While that is true, locking has a great deal of overhead, and so it might not be a bad idea to create thread unsafe versions of the code, first, and then create a lightweight threadsafe wrapper for it.
@Michael: if your mutex implementation supports recursive locking at all, chances are it will support it efficiently. All that's usually required is that the lock function does "if (mutex.owner == thisthread) { ++lockcount; return; }". If you don't have the lock then you're reading the owner field unsynchronized, but provided the implementation knows that reads and writes of the mutex.owner field are atomic (e.g. word reads on x86), you can't get a false positive. If you do have the lock then you can't get a false negative because nothing can change the owner while you hold it.
"you can't get a false positive". I should probably mention that there are some further assumptions there - primarily that each time a thread acquires or releases a mutex, the kernel ensures that the mutex.owner field is changed in a way which is visible to that thread (either change it from the thread, or flush the thread's cache of it, or whatever).
As for "acquisition and release impose a great deal of overhead", depends on the context. In the case of a futex in a single-threaded use (so zero contention), it's pretty fast, probably not worth worrying about unless you're also carefully avoiding a lot of other things. Easy enough to measure - if all else fails just comment out the lock/unlock. But if you're in a code where for example a cache miss would ruin your day, I expect it's a concern.
Providing private unlocked versions of some public API functions (or public methods) is lesser evil. In most cases it is not that messy to have public foo() lock, call private foo_unlocked() and unlock. If you have many such duplications you could minimize the overhead by using macros to define both public locking and private nonlocking versions of functions (declaration and definition). Using recursive mutexes encourages you to not think your concurrency design clearly. There is no long term benefit in using recursive mutexes. Recursive mutexes are technical debt.
c
comonad

Recursive and non-recursive mutexes have different use cases. No mutex type can easily replace the other. Non-recursive mutexes have less overhead, and recursive mutexes have in some situations useful or even needed semantics and in other situations dangerous or even broken semantics. In most cases, someone can replace any strategy using recursive mutexes with a different safer and more efficient strategy based on the usage of non-recursive mutexes.

If you just want to exclude other threads from using your mutex protected resource, then you could use any mutex type, but might want to use the non-recursive mutex because of its smaller overhead.

If you want to call functions recursively, which lock the same mutex, then they either have to use one recursive mutex, or have to unlock and lock the same non-recursive mutex again and again (beware of concurrent threads!) (assuming this is semantically sound, it could still be a performance issue), or have to somehow annotate which mutexes they already locked (simulating recursive ownership/mutexes).

have to use one recursive mutex, or

have to unlock and lock the same non-recursive mutex again and again (beware of concurrent threads!) (assuming this is semantically sound, it could still be a performance issue), or

have to somehow annotate which mutexes they already locked (simulating recursive ownership/mutexes).

If you want to lock several mutex-protected objects from a set of such objects, where the sets could have been built by merging, you can choose to use per object exactly one mutex, allowing more threads to work in parallel, or to use per object one reference to any possibly shared recursive mutex, to lower the probability of failing to lock all mutexes together, or to use per object one comparable reference to any possibly shared non-recursive mutex, circumventing the intent to lock multiple times.

to use per object exactly one mutex, allowing more threads to work in parallel, or

to use per object one reference to any possibly shared recursive mutex, to lower the probability of failing to lock all mutexes together, or

to use per object one comparable reference to any possibly shared non-recursive mutex, circumventing the intent to lock multiple times.

If you want to release a lock in a different thread than it has been locked, then you have to use non-recursive locks (or recursive locks which explicitly allow this instead of throwing exceptions).

If you want to use synchronization variables, then you need to be able to explicitly unlock the mutex while waiting on any synchronization variable, so that the resource is allowed to be used in other threads. That is only sanely possible with non-recursive mutexes, because recursive mutexes could already have been locked by the caller of the current function.


Not sure I agree that "have to unlock and lock the same non-recursive mutex again and again (beware of concurrent threads!)" is viable, even with the warning. If you're not ready to release the resource (either it's in an inconsistent state or you don't want anyone else mucking about with it), do not unlock/recur/lock. I guarantee at some point another thread will sneak in and bite you where the sun don't shine :-) I won't downvote since the rest of the answer is actually very good - I just thought I'd bring that up.
See stackoverflow.com/questions/10546867/… for what bought this on.
You should mention, that taking multiple mutexes for multiple objects can easily lead to deadlocks if not everybody follows same locking order
D
Doktor Schrott

I encountered the need for a recursive mutex today, and I think it's maybe the simplest example among the posted answers so far: This is a class that exposes two API functions, Process(...) and reset().

public void Process(...)
{
  acquire_mutex(mMutex);
  // Heavy processing
  ...
  reset();
  ...
  release_mutex(mMutex);
}

public void reset()
{
  acquire_mutex(mMutex);
  // Reset
  ...
  release_mutex(mMutex);
}

Both functions must not run concurrently because they modify internals of the class, so I wanted to use a mutex. Problem is, Process() calls reset() internally, and it would create a deadlock because mMutex is already acquired. Locking them with a recursive lock instead fixes the problem.


Just make a private version of reset() that will not lock the mutex for internal use. Public API reset() to lock the mutex and call the internal reset and unlock the mutex. This is ridiculous reason to introduce recursive mutexes. To maximize parallelism you should hold the lock as small amount of time as possible, recursive mutexes do not help with this - on the contrary.
How long one holds to a mutex is heavily dependent on the task at hand. Maybe you only code GUI; other people code heavy processing items that need to be mutexed.
mutexes are concurrency killers, that's the whole point of them. As soon as your invariances are in good shape you unlock so that other heavy processing threads can do their work. See great David Butenhof talking about recursive mutexes: zaval.org/resources/library/butenhof1.html (they should only be used as a "crutch" until you get non-thread safe code refactored in thread-safe state with specific locks, he thinks also that in this case you only need a single global lock).
I can see the value of that approach, but keep in mind that it comes af the price of having two separate functions now, solely to avoid (what seems to me) an ideological avoidance of a recursive mutex.
In my experience the price is negligible in practise. Typically, since you want to acquire the mutexes for the most minimal time possible, we are anyway talking of inline functions when programming in C++. Antirecursivemutexist? There is nothing ideological wanting to avoid recursive mutexes. It is about wanting to keep your design clean and correct. I still recommend reading Butenhof's reasoning on the subject.
F
FormerAtariUser

If you want to see an example of code that uses recursive mutexes, look at the sources for "Electric Fence" for Linux/Unix. 'Twas one of the common Unix tools for finding "bounds checking" read/write overruns and underruns as well as using memory that has been freed, before Valgrind came along.

Just compile and link electric fence with sources (option -g with gcc/g++), and then link it with your software with the link option -lefence, and start stepping through the calls to malloc/free. http://elinux.org/Electric_Fence


M
Michael Burr

It would certainly be a problem if a thread blocked trying to acquire (again) a mutex it already owned...

Is there a reason to not permit a mutex to be acquired multiple times by the same thread?


Simplicity of implementation, I think. Also, here's a reasonable factor against recursive mutexes: stackoverflow.com/questions/187761/…. I suspect Java may have pretty much obliterated the average programmer's familiarity with, or ability to use, non-recursive locks.
C++ is designed to be a very fast language. Unlike some other popular languages, C++ tries to provide bare-bones implementations (such as std::mutex) that do their fundamental job as fast as possible. If you need to use the more featured object (like std::recursive_mutex) at a small cost of overhead, it is also available. The point is to give the developer the ability to write blazingly fast code when they need to.
F
Fernando Andreas Sahmkow Beico

In general, like everyone here said, it's more about design. A recursive mutex is normally used in a recursive functions.

What others fail to tell you here is that there's actually almost no cost overhead in recursive mutexes.

In general, a simple mutex is a 32 bits key with bits 0-30 containing owner's thread id and bit 31 a flag saying if the mutex has waiters or not. It has a lock method which is a CAS atomic race to claim the mutex with a syscall in case of failure. The details are not important here. It looks like this:

class mutex {
public:
  void lock();
  void unlock();
protected:
  uint32_t key{}; //bits 0-30: thread_handle, bit 31: hasWaiters_flag
};

a recursive_mutex is normally implemented as:

class recursive_mutex : public mutex {
public:
  void lock() {
    uint32_t handle = current_thread_native_handle(); //obtained from TLS memory in most OS
    if ((key & 0x7FFFFFFF) == handle) { // Impossible to return true unless you own the mutex.
      uses++; // we own the mutex, just increase uses.
    } else {
      mutex::lock(); // we don't own the mutex, try to obtain it.
      uses = 1;
    }
  }

  void unlock() {
    // asserts for debug, we should own the mutex and uses > 0
    --uses;
    if (uses == 0) {
      mutex::unlock();
    }
  }
private:
  uint32_t uses{}; // no need to be atomic, can only be modified in exclusion and only interesting read is on exclusion.
};

As you see it's an entirely user space construct. (base mutex is not though, it MAY fall into a syscall if it fails to obtain the key in an atomic compare and swap on lock and it will do a syscall on unlock if the has_waitersFlag is on).

For a base mutex implementation: https://github.com/switchbrew/libnx/blob/master/nx/source/kernel/mutex.c


l
lumpidu

If you want to be able to call public methods from different threads inside other public methods of a class and many of these public methods change the state of the object, you should use a recursive mutex. In fact, I make it a habit of using by default a recursive mutex unless there is a good reason (e.g. special performance considerations) not to use it.

It leads to better interfaces, because you don't have to split your implementation among non-locked and locked parts and you are free to use your public methods with peace of mind inside all methods as well.

It leads also in my experience to interfaces that are easier to get right in terms of locking.


C
Cavaler

Seems no one mentioned it before, but code using recursive_mutex is way easier to debug, since its internal structure contains identifier of a thread holding it.