68

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

4

8 回答 8

55

例如,当您有递归调用它的函数,并且您想要同步访问它时:

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

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

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

void foo() { ... foo(); ... }
于 2010-03-10T07:03:47.140 回答
23

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

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

我今天遇到了对递归互斥锁的需求,我认为这可能是迄今为止发布的答案中最简单的示例:这是一个公开两个 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 已经被获取。用递归锁锁定它们可以解决问题。

于 2015-05-08T17:16:01.933 回答
3

如果您想查看使用递归互斥锁的代码示例,请查看适用于 Linux/Unix 的“Electric Fence”的源代码。在Valgrind出现之前,它是用于查找“边界检查”读/写溢出和欠载以及使用已释放内存的常用 Unix 工具之一。

只需编译并将电子围栏与源代码链接(选项 -g 与 gcc/g++),然后使用链接选项 -lefence 将其与您的软件链接,并开始逐步调用 malloc/free。http://elinux.org/Electric_Fence

于 2012-04-27T15:44:27.087 回答
2

如果线程阻塞尝试(再次)获取它已经拥有的互斥锁,那肯定会是一个问题......

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

于 2010-03-10T07:08:00.620 回答
0

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

它会带来更好的接口,因为您不必在非锁定和锁定部分之间拆分实现,并且您也可以在所有方法中安心地使用公共方法。

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

于 2020-08-20T09:15:10.110 回答
0

总的来说,就像这里的每个人所说的那样,它更多的是关于设计。递归互斥锁通常用于递归函数。

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

一般来说,一个简单的互斥锁是一个 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

于 2021-04-12T13:55:34.613 回答
0

之前似乎没有人提到它,但是使用 recursive_mutex 的代码更容易调试,因为它的内部结构包含持有它的线程的标识符。

于 2022-03-02T09:26:23.600 回答