何时使用递归互斥?

我理解递归互斥锁允许互斥锁被锁定不止一次而不会陷入死锁,并且应该解锁相同次数的互斥锁。但是在什么特定的情况下需要使用递归互斥锁呢?我正在寻找设计/代码级别的情况。

64277 次浏览

例如,当你有一个递归调用它的函数,你想同步访问它:

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

如果没有递归互斥锁,那么必须首先创建一个“入口点”函数,如果有一组相互递归的函数,这就变得很麻烦。没有递归互斥:

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


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

如果一个线程试图(再一次)获取它已经拥有的互斥对象而被阻塞,这肯定会是一个问题... ..。

是否有理由不允许同一个线程多次获取互斥对象?

递归和非递归互斥锁具有 不同的用例。没有互斥体类型可以轻易地替换另一个。非递归互斥锁的开销较小,递归互斥锁在某些情况下具有有用的甚至需要的语义,在其他情况下具有危险的甚至破坏的语义。在大多数情况下,有人可以根据非递归互斥锁的使用,用另一种更安全、更有效的策略替换使用递归互斥锁的任何策略。

  • 如果只想排除使用互斥锁保护资源的其他线程,那么可以使用 任何互斥体类型,但可能需要使用非递归互斥锁,因为它的开销较小。
  • 如果要递归地调用锁定相同互斥锁的函数,那么它们
    • 必须使用 一个递归互斥对象,或
    • 必须一次又一次地解锁和锁定相同的非递归互斥锁(小心并发线程!)(假设这在语义上是合理的,它仍然可能是一个性能问题) ,或者
    • 必须以某种方式注释它们已经锁定的互斥对象(模拟递归所有权/互斥对象)。
  • 如果希望从这样的对象集中锁定几个互斥体保护的对象,而这些对象集可以通过合并来构建,则可以选择
    • 确切地使用每个对象 一个互斥体,允许更多的线程并行工作,或者
    • 对任何 可能共享的递归互斥锁使用每个对象 一个推荐信,以降低不能将所有互斥锁锁定在一起的可能性,或者
    • 对任何 可能共享的非递归互斥锁使用每个对象 一个类似的参考,避免了多次锁定的意图。
  • 如果希望在不同的线程中释放锁,而不是在已经锁定的线程中释放锁,那么必须使用非递归锁(或者显式允许这样做的递归锁,而不是抛出异常)。
  • 如果您想使用 同步变量,那么在等待任何同步变量时,您需要成为 能够显式地解锁互斥锁,这样才能允许在其他线程中使用该资源。这只有在 非递归互斥锁中才能实现,因为递归互斥锁可能已经被当前函数的调用方锁定。

如果您希望看到使用递归互斥锁的代码示例,请查看 Linux/Unix 的“ Electric Fence”源代码。在 瓦尔荷恩出现之前,‘ T 是一种常用的 Unix 工具,用于查找“边界检查”读/写溢出和底溢出,以及使用已释放的内存。

只要编译和链接电子篱笆与源代码(选项-g 与 gcc/g + +) ,然后链接到您的软件与链接选项-左栏,并开始步骤通过调用 malloc/free。Http://elinux.org/electric_fence

今天我遇到了一个递归互斥对象的需求,我认为这可能是目前为止发布的答案中最简单的例子: 这是一个公开了两个 API 函数的类,Process (...)和 set ()。

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


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

这两个函数不能同时运行,因为它们修改了类的内部结构,所以我想使用互斥锁。 问题是,Process ()在内部调用 set () ,并且由于已经获取了 mMutex,它将创建一个死锁。 用递归锁锁定它们可以解决这个问题。

如果希望能够从类的其他公共方法中的不同线程调用公共方法,并且这些公共方法中的许多都会更改对象的状态,则应该使用递归互斥锁。事实上,我习惯在缺省情况下使用递归互斥锁,除非有充分的理由(例如,特殊的性能考虑)不使用它。

它带来了更好的接口,因为您不必在非锁定部分和锁定部分之间分割实现,并且您可以在所有方法内部自由地使用公共方法。

根据我的经验,它还使得接口在锁定方面更容易正确。

一般来说,就像这里的每个人说的,它更多的是关于设计。递归互斥锁通常用在递归函数中。

其他人没有告诉你的是,这里实际上有 在递归互斥锁中几乎没有成本开销

一般来说,一个简单的互斥锁是一个32位的键,其中0-30位包含所有者的线程 ID,31位标记表示互斥锁是否有等待者。它有一个 lock 方法,这是一个 CAS 原子竞赛,在发生故障时用一个 syscall 声明互斥锁。细节不重要。它看起来像这样:

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

递归 _ 互斥对象通常实现为:

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.
};

如您所见,它完全是一个用户空间构造。(base mutex 不是这样的,如果它在原子比较和锁定交换中没有获得密钥,它可能会进入系统调用,如果 has _ waitersFlag 打开,它会在解锁时进行系统调用)。

对于基互斥锁实现: https://github.com/switchbrew/libnx/blob/master/nx/source/kernel/mutex.c

似乎以前没有人提到过这个问题,但是使用递归 _ mutex 的代码更容易调试,因为它的内部结构包含持有它的线程的标识符。