我了解递归互斥锁允许互斥锁多次锁定而不会陷入死锁,并且应该解锁相同的次数。但是在什么特定情况下需要使用递归互斥锁呢?我正在寻找设计/代码级别的情况。
8 回答
例如,当您有递归调用它的函数,并且您想要同步访问它时:
void foo() {
... mutex_acquire();
... foo();
... mutex_release();
}
如果没有递归互斥锁,您必须首先创建一个“入口点”函数,当您拥有一组相互递归的函数时,这会变得很麻烦。没有递归互斥锁:
void foo_entry() {
mutex_acquire(); foo(); mutex_release(); }
void foo() { ... foo(); ... }
递归和非递归互斥锁有不同的用例。没有互斥类型可以轻松替换其他类型。非递归互斥体的开销较小,递归互斥体在某些情况下具有有用甚至需要的语义,而在其他情况下则具有危险甚至破坏的语义。在大多数情况下,有人可以根据非递归互斥锁的使用,将使用递归互斥锁的任何策略替换为不同的更安全、更有效的策略。
- 如果您只想排除其他线程使用受互斥体保护的资源,那么您可以使用任何互斥体类型,但可能希望使用非递归互斥体,因为它的开销较小。
- 如果你想递归调用函数来锁定同一个互斥锁,那么它们要么
- 必须使用一个递归互斥锁,或
- 必须一次又一次地解锁和锁定同一个非递归互斥体(当心并发线程!)(假设这在语义上是合理的,它仍然可能是一个性能问题),或者
- 必须以某种方式注释它们已经锁定的互斥锁(模拟递归所有权/互斥锁)。
- 如果您想从一组这样的对象中锁定几个受互斥保护的对象,这些对象可以通过合并来构建,您可以选择
- 为每个对象使用一个 mutex,允许更多线程并行工作,或者
- 为每个对象使用一个对任何可能共享的递归互斥锁的引用,以降低无法将所有互斥锁锁定在一起的可能性,或者
- 为每个对象使用一个对任何可能共享的非递归互斥体的可比引用,从而避免多次锁定的意图。
- 如果您想在与已锁定不同的线程中释放锁,则必须使用非递归锁(或明确允许这样做而不是抛出异常的递归锁)。
- 如果要使用同步变量,则需要能够在等待任何同步变量时显式解锁互斥锁,以便允许在其他线程中使用该资源。这只有在使用非递归互斥锁时才可能实现,因为递归互斥锁可能已经被当前函数的调用者锁定。
我今天遇到了对递归互斥锁的需求,我认为这可能是迄今为止发布的答案中最简单的示例:这是一个公开两个 API 函数的类,Process(...) 和 reset()。
public void Process(...)
{
acquire_mutex(mMutex);
// Heavy processing
...
reset();
...
release_mutex(mMutex);
}
public void reset()
{
acquire_mutex(mMutex);
// Reset
...
release_mutex(mMutex);
}
这两个函数不能同时运行,因为它们修改了类的内部,所以我想使用互斥锁。问题是,Process() 在内部调用了 reset(),它会造成死锁,因为 mMutex 已经被获取。用递归锁锁定它们可以解决问题。
如果您想查看使用递归互斥锁的代码示例,请查看适用于 Linux/Unix 的“Electric Fence”的源代码。在Valgrind出现之前,它是用于查找“边界检查”读/写溢出和欠载以及使用已释放内存的常用 Unix 工具之一。
只需编译并将电子围栏与源代码链接(选项 -g 与 gcc/g++),然后使用链接选项 -lefence 将其与您的软件链接,并开始逐步调用 malloc/free。http://elinux.org/Electric_Fence
如果线程阻塞尝试(再次)获取它已经拥有的互斥锁,那肯定会是一个问题......
是否有理由不允许同一个线程多次获取互斥锁?
如果您希望能够从类的其他公共方法中的不同线程调用公共方法,并且其中许多公共方法会更改对象的状态,则应该使用递归互斥锁。事实上,我养成了默认使用递归互斥锁的习惯,除非有充分的理由(例如特殊的性能考虑)不使用它。
它会带来更好的接口,因为您不必在非锁定和锁定部分之间拆分实现,并且您也可以在所有方法中安心地使用公共方法。
根据我的经验,它还导致在锁定方面更容易获得正确的接口。
总的来说,就像这里的每个人所说的那样,它更多的是关于设计。递归互斥锁通常用于递归函数。
其他人在这里没有告诉您的是,递归互斥锁实际上几乎没有成本开销。
一般来说,一个简单的互斥锁是一个 32 位的密钥,其中 0-30 位包含所有者的线程 ID,第 31 位是一个标志,表示互斥锁是否有服务员。它有一个锁定方法,这是一种 CAS 原子竞赛,可以在失败的情况下通过系统调用来声明互斥锁。细节在这里并不重要。它看起来像这样:
class mutex {
public:
void lock();
void unlock();
protected:
uint32_t key{}; //bits 0-30: thread_handle, bit 31: hasWaiters_flag
};
recursive_mutex 通常实现为:
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.
};
如您所见,它完全是一个用户空间结构。(虽然基本互斥锁不是,但如果它无法在原子比较中获取密钥并在锁定时交换,它可能会陷入系统调用,如果 has_waitersFlag 开启,它将在解锁时执行系统调用)。
对于基本互斥锁实现:https ://github.com/switchbrew/libnx/blob/master/nx/source/kernel/mutex.c
之前似乎没有人提到它,但是使用 recursive_mutex 的代码更容易调试,因为它的内部结构包含持有它的线程的标识符。