何时使用递归互斥锁?

发布于 2024-08-24 04:04:27 字数 81 浏览 4 评论 0原文

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

I understand recursive mutex allows mutex to be locked more than once without getting to a deadlock and should be unlocked the same number of times. But in what specific situations do you need to use a recursive mutex? I'm looking for design/code-level situations.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

白衬杉格子梦 2024-08-31 04:04:27

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

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

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

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

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

For example when you have function that calls it recursively, and you want to get synchronized access to it:

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

without a recursive mutex you would have to create an "entry point" function first, and this becomes cumbersome when you have a set of functions that are mutually recursive. Without recursive mutex:

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

void foo() { ... foo(); ... }
各自安好 2024-08-31 04:04:27

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

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

Recursive and non-recursive mutexes have different use cases. No mutex type can easily replace the other. Non-recursive mutexes have less overhead, and recursive mutexes have in some situations useful or even needed semantics and in other situations dangerous or even broken semantics. In most cases, someone can replace any strategy using recursive mutexes with a different safer and more efficient strategy based on the usage of non-recursive mutexes.

  • If you just want to exclude other threads from using your mutex protected resource, then you could use any mutex type, but might want to use the non-recursive mutex because of its smaller overhead.
  • If you want to call functions recursively, which lock the same mutex, then they either
    • have to use one recursive mutex, or
    • have to unlock and lock the same non-recursive mutex again and again (beware of concurrent threads!) (assuming this is semantically sound, it could still be a performance issue), or
    • have to somehow annotate which mutexes they already locked (simulating recursive ownership/mutexes).
  • If you want to lock several mutex-protected objects from a set of such objects, where the sets could have been built by merging, you can choose
    • to use per object exactly one mutex, allowing more threads to work in parallel, or
    • to use per object one reference to any possibly shared recursive mutex, to lower the probability of failing to lock all mutexes together, or
    • to use per object one comparable reference to any possibly shared non-recursive mutex, circumventing the intent to lock multiple times.
  • If you want to release a lock in a different thread than it has been locked, then you have to use non-recursive locks (or recursive locks which explicitly allow this instead of throwing exceptions).
  • If you want to use synchronization variables, then you need to be able to explicitly unlock the mutex while waiting on any synchronization variable, so that the resource is allowed to be used in other threads. That is only sanely possible with non-recursive mutexes, because recursive mutexes could already have been locked by the caller of the current function.
蓝海 2024-08-31 04:04:27

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

I encountered the need for a recursive mutex today, and I think it's maybe the simplest example among the posted answers so far:
This is a class that exposes two API functions, Process(...) and reset().

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

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

Both functions must not run concurrently because they modify internals of the class, so I wanted to use a mutex.
Problem is, Process() calls reset() internally, and it would create a deadlock because mMutex is already acquired.
Locking them with a recursive lock instead fixes the problem.

歌枕肩 2024-08-31 04:04:27

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

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

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

In general, like everyone here said, it's more about design. A recursive mutex is normally used in a recursive functions.

What others fail to tell you here is that there's actually almost no cost overhead in recursive mutexes.

In general, a simple mutex is a 32 bits key with bits 0-30 containing owner's thread id and bit 31 a flag saying if the mutex has waiters or not. It has a lock method which is a CAS atomic race to claim the mutex with a syscall in case of failure. The details are not important here. It looks like this:

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

a recursive_mutex is normally implemented as:

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

As you see it's an entirely user space construct. (base mutex is not though, it MAY fall into a syscall if it fails to obtain the key in an atomic compare and swap on lock and it will do a syscall on unlock if the has_waitersFlag is on).

For a base mutex implementation: https://github.com/switchbrew/libnx/blob/master/nx/source/kernel/mutex.c

于我来说 2024-08-31 04:04:27

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

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

If you want to see an example of code that uses recursive mutexes, look at the sources for "Electric Fence" for Linux/Unix. 'Twas one of the common Unix tools for finding "bounds checking" read/write overruns and underruns as well as using memory that has been freed, before Valgrind came along.

Just compile and link electric fence with sources (option -g with gcc/g++), and then link it with your software with the link option -lefence, and start stepping through the calls to malloc/free. http://elinux.org/Electric_Fence

尐籹人 2024-08-31 04:04:27

如果一个线程试图(再次)获取它已经拥有的互斥体而被阻止,那肯定会出现问题......

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

It would certainly be a problem if a thread blocked trying to acquire (again) a mutex it already owned...

Is there a reason to not permit a mutex to be acquired multiple times by the same thread?

沙沙粒小 2024-08-31 04:04:27

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

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

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

If you want to be able to call public methods from different threads inside other public methods of a class and many of these public methods change the state of the object, you should use a recursive mutex. In fact, I make it a habit of using by default a recursive mutex unless there is a good reason (e.g. special performance considerations) not to use it.

It leads to better interfaces, because you don't have to split your implementation among non-locked and locked parts and you are free to use your public methods with peace of mind inside all methods as well.

It leads also in my experience to interfaces that are easier to get right in terms of locking.

巾帼英雄 2024-08-31 04:04:27

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

Seems no one mentioned it before, but code using recursive_mutex is way easier to debug, since its internal structure contains identifier of a thread holding it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文