什么时候使用递归互斥?

时间:2010-03-10 07:01:25

标签: c++ multithreading recursion mutex recursive-mutex

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

7 个答案:

答案 0 :(得分:45)

例如,如果你有一个递归调用它的函数,并且你想获得它的同步访问:

void foo() {
   ... mutex_acquire();
   ... foo();
   ... mutex_release();
}
如果没有递归的互斥锁,你必须首先创建一个“入口点”函数,当你拥有一组相互递归的函数时,这会变得很麻烦。没有递归互斥:

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

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

答案 1 :(得分:23)

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

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

答案 2 :(得分:3)

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

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

答案 3 :(得分:3)

我今天遇到了递归互斥锁的需要,我认为它可能是到目前为止发布的答案中最简单的例子: 这是一个公开两个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。 使用递归锁定它们可以解决问题。

答案 4 :(得分:2)

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

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

答案 5 :(得分:0)

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

这会带来更好的界面,因为您不必在非锁定和锁定部分之间拆分实现,并且可以自由地在所有方法内部使用公共方法。

在我的经验中,它也使界面更易于锁定。

答案 6 :(得分: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