什么时候使用递归互斥?

jas*_*ine 56 c++ recursion multithreading mutex recursive-mutex

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

Ant*_*ima 48

例如,当您具有递归调用它的函数,并且您希望获得对它的同步访问:

void foo() {
   ... mutex_acquire();
   ... foo();
   ... mutex_release();
}
Run Code Online (Sandbox Code Playgroud)

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

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

void foo() { ... foo(); ... }
Run Code Online (Sandbox Code Playgroud)

  • 虽然这是事实,但锁定会产生大量开销,因此首先创建代码的线程不安全版本可能并不是一个坏主意,然后为它创建一个轻量级线程安全包装器. (6认同)
  • @Michael:如果您的互斥锁实现完全支持递归锁定,那么它很可能会有效地支持它.所有通常需要的是锁定函数执行"if(mutex.owner == thisthread){++ lockcount; return;}".如果您没有锁,那么您正在读取所有者字段未同步,但是如果实现知道mutex.owner字段的读取和写入是原子的(例如x86上的单词读取),则不能得到错误正.如果您确实拥有锁定,那么您就不能得到假阴性,因为在您持有它时没有任何东西可以改变所有者. (5认同)

com*_*nad 23

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

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

  • 不确定我是否同意"必须一次又一次地解锁和锁定相同的非递归互斥锁(谨防并发线程!)"即使有警告也是可行的.如果您还没有准备好释放资源(要么是处于不一致状态,要么您不希望其他任何人使用它),请执行_not_ unlock/recur/lock.我保证在某个时候另一个线程会潜入并咬你太阳不发光的地方:-)我不会贬低,因为其余的答案实际上非常好 - 我以为我会把它拿出来. (5认同)

Dok*_*ott 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);
}
Run Code Online (Sandbox Code Playgroud)

这两个函数不能同时运行,因为它们修改了类的内部结构,所以我想使用互斥锁。问题是,Process() 在内部调用 reset(),它会造成死锁,因为 mMutex 已经被获取。用递归锁锁定它们可以解决问题。

  • 只需制作一个私有版本的`reset()`,它不会锁定内部使用的互斥锁。公共 API `reset()` 来锁定互斥锁并调用内部重置和解锁互斥锁。这是引入递归互斥锁的荒谬理由。为了最大限度地提高并行性,您应该尽可能少地持有锁,递归互斥锁对此无济于事 - 恰恰相反。 (6认同)
  • 持有互斥锁的时间在很大程度上取决于手头的任务。也许你只编写 GUI;其他人编码需要互斥的繁重处理项目。 (2认同)
  • 根据我的经验,价格在实践中可以忽略不计。通常,由于您希望在尽可能短的时间内获取互斥锁,因此在使用 C++ 编程时,我们无论如何都在谈论内联函数。反递归互存?没有任何意识形态想要避免递归互斥锁。这是关于想要保持你的设计干净和正确。我仍然建议阅读 Butenhof 关于这个主题的推理。 (2认同)

Fer*_*ico 5

总的来说,正如这里的每个人所说,更多的是设计。递归互斥体通常用在递归函数中。

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

一般来说,简单的互斥锁是一个 32 位密钥,其中第 0-30 位包含所有者的线程 ID,第 31 位是表示互斥锁是否有等待者的标志。它有一个 lock 方法,该方法是 CAS 原子竞赛,用于在失败时通过系统调用声明互斥锁。细节在这里并不重要。它看起来像这样:

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

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.
};
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,它完全是一个用户空间构造。(但是基本互斥体不是,如果它无法在原子比较和交换锁定中获取密钥,它可能会陷入系统调用,并且如果 has_waitersFlag 处于打开状态,它将在解锁时执行系统调用)。

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