c++ 什么时候使用递归互斥?

dffbzjpn  于 12个月前  发布在  其他
关注(0)|答案(8)|浏览(146)

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

6tr1vspr

6tr1vspr1#

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

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

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

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

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

exdqitrt2#

递归和非递归互斥有不同的用例。没有一种互斥类型可以轻易地取代另一种。非递归互斥锁的开销更少,而递归互斥锁在某些情况下有有用的甚至是需要的语义,而在其他情况下有危险的甚至是破坏的语义。在大多数情况下,有人可以用基于非递归互斥锁的使用的不同的更安全和更有效的策略来替换使用递归互斥锁的任何策略。

  • 如果你只是想排除其他线程使用你的互斥保护资源,那么你可以使用任何互斥类型,但可能想使用非递归互斥,因为它的开销较小。
  • 如果你想递归地调用函数,它们会锁定同一个互斥体,那么它们要么
  • 必须使用一个递归互斥量,或者
  • 必须一次又一次地解锁和锁定同一个非递归互斥锁(小心并发线程!)(假设这在语义上是合理的,它仍然可能是一个性能问题),或者
  • 必须以某种方式注解它们已经锁定的互斥锁(模拟递归所有权/互斥锁)。
  • 如果要从一组受互斥保护的对象中锁定几个受互斥保护的对象(这些对象可以通过合并生成),可以选择
  • 每个对象只使用一个互斥量,允许多个线程并行工作,或者
  • 对每个对象使用一个引用到任何 * 可能共享的 * 递归互斥锁**,以降低无法将所有互斥锁锁定在一起的概率,或者
  • 对每个对象使用一个可比较的引用到任何 * 可能共享的 * 非递归互斥体**,避免多次锁定的意图。
  • 如果你想在一个不同的线程中释放一个锁,而不是它被锁定,那么你必须使用非递归锁(或者显式允许这样做而不是抛出异常的递归锁)。
  • 如果你想使用 * 同步变量 *,那么你需要在等待任何同步变量的时候能够显式地解锁互斥锁,这样资源才能被其他线程使用。这只有在非递归互斥锁的情况下才有可能,因为递归互斥锁可能已经被当前函数的调用者锁定了。
slmsl1lt

slmsl1lt3#

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

23c0lvtd

23c0lvtd4#

如果您想查看使用递归互斥锁的代码示例,请查看Linux/Unix的“Electric Fence”的源代码。在Valgrind沿着出现之前,这是一个常见的Unix工具,用于查找“边界检查”读/写溢出和欠载,以及使用已释放的内存。
只需编译并将electric fence与源代码链接(gcc/g++中的选项-g),然后使用链接选项-lefence将其与您的软件链接,并开始逐步执行对malloc/free的调用。http://elinux.org/Electric_Fence

fafcakar

fafcakar5#

如果一个线程试图(再次)获取它已经拥有的互斥体时被阻塞,这肯定是一个问题。
是否有理由不允许一个互斥体被同一个线程多次获取?

8gsdolmq

8gsdolmq6#

总的来说,就像这里的每个人说的,它更多的是关于设计。递归互斥体通常用于递归函数中。
其他人没有告诉你的是,递归互斥锁实际上几乎没有开销
一般来说,一个简单的互斥体是一个32位的密钥,0-30位包含所有者的线程id,31位是一个标志,说明互斥体是否有等待者。它有一个锁方法,这是一个CAS原子竞赛,在失败的情况下使用syscall声明互斥锁。细节在这里并不重要。它看起来像这样:

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

erhoui1w

erhoui1w7#

如果你希望能够在一个类的其他公共方法中调用来自不同线程的公共方法,并且这些公共方法中的许多方法会改变对象的状态,那么你应该使用递归互斥体。事实上,我习惯于默认使用递归互斥,除非有很好的理由(例如:特殊性能考虑)不使用它。
它带来了更好的接口,因为你不必在非锁定和锁定部分之间分割你的实现,并且你可以在所有方法中自由地使用你的公共方法。
根据我的经验,它还导致了在锁定方面更容易获得正确的接口。

sbdsn5lh

sbdsn5lh8#

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

相关问题