非递归互斥锁的所有权

3

我在stackoverflow上读到了这个答案:

由于递归互斥锁具有所有权意义,因此抓取互斥锁的线程必须是释放互斥锁的同一线程。对于非递归互斥锁,没有所有权感,任何线程通常都可以释放互斥锁,无论最初哪个线程获取了互斥锁。

我对最后一句话感到困惑。一个线程可以锁定一个互斥锁,而另一个不同的线程可以解锁该互斥锁吗?我认为只有同一线程才能解锁互斥锁?还是有特定的互斥锁可以允许这样做?希望有人可以澄清。


顺便说一下,我的问题来源于这个线程中的答案:https://dev59.com/wHVC5IYBdhLWcg3wxEN1 - jasonline
3个回答

6

非递归互斥锁

大多数互斥锁都是(或者至少应该是)非递归的。互斥锁是一种可以原子性地获取或释放的对象,它可以保护在多个线程之间共享的数据免受竞态条件、数据损坏和其他不良影响。

同一个线程在同一调用链中只能获取一次单个互斥锁。试图在同一线程上下文中多次获取(或持有)同一个互斥锁应被视为无效场景,并应适当处理(通常通过断言来处理,因为您正在违反代码的基本约定)。

递归互斥锁

这应该被视为代码异味或骚操作。递归互斥锁与标准互斥锁唯一的区别在于,同一线程可以多次获取递归互斥锁。

需要递归互斥锁的根本原因是缺乏所有权,没有明确的目的或类之间的界限。例如,您的代码可能调用另一个类,然后该类再次调用回您的类。起始类随后可能会尝试再次获取相同的互斥锁,为了避免崩溃,您将其实现为递归互斥锁。

这种颠倒的类层次结构可能会导致各种头疼问题,而递归互斥锁只是为更基本的架构问题提供了一个临时解决方案。


无论互斥锁类型如何,总是应该由相同的线程获取和释放同一个互斥锁。您在代码中使用的一般模式如下:

Thread 1

    Acquire mutex A
    // Modify or read shared data
    Release mutex A

Thread 2

    Attempt to acquire mutex A
    Block as thread 1 has mutex A
    When thread 1 has released mutex A, acquire it
    // Modify or read shared data
    Release mutex A

当你有多个可以同时获取的互斥锁(比如说,mutex A 和 B),问题就变得更加复杂了。这时有可能会遇到死锁的情况,类似于下面的例子:

Thread 1

    Acquire mutex A
    // Access some data...

*** Context switch to thread 2 ***

Thread 2

    Acquire mutex B
    // Access some data

*** Context switch to thread 1 ***

    Attempt to acquire mutex B
    Wait for thread 2 to release mutex B

*** Context switch to thread 2 ***

    Attempt to acquire mutex A
    Wait for thread 1 to release mutex A

*** DEADLOCK ***

现在我们面临的情况是每个线程都在等待另一个线程释放另一个锁 -- 这被称为ABBA死锁模式

为了防止这种情况发生,重要的是每个线程总是按照相同的顺序获取互斥锁(例如,始终先A,然后B)。


@jasonline - 是的,互斥锁的本质就是获取它的线程会释放它。如果另一个线程可以释放由另一个线程获取的互斥锁,那么互斥锁还有什么意义呢? - LeopardSkinPillBoxHat
互斥锁是一种特定类型的信号量,它包含了额外的功能 - 有关更多信息,请参阅此文章:http://en.wikipedia.org/wiki/Semaphore_%28programming%29。 - LeopardSkinPillBoxHat
@JohnKugelman - 我会通过创建一个API层(前端)和一个实现层(后端)来避免使用递归互斥锁。您可以强制执行策略,即锁始终由实现层获取,这意味着第二个公共方法只会在第一个公共方法的实现层中获取互斥锁一次。 - LeopardSkinPillBoxHat
是的,我认为很清楚:互斥锁仅用于互斥(post/release 操作仅限于调用 pend/acquire 的线程),而二进制信号量则用于事件通知(任何线程都可以进行 post 操作)和互斥。 - jasonline
@JohnKugelman - 我认为递归互斥锁对我来说已经失去了光辉,因为我在使用它们的代码中遇到了很多问题(主要是在多线程应用程序中死锁)。 对于简单的API,只要以一致的方式获取它们(并且正确的策略得到了充分记录),它们可能不会给您带来问题。 我所工作的应用程序使用它们来保护整个应用程序中的重要数据结构,这就是我们遇到如此多头痛的原因。 - LeopardSkinPillBoxHat
显示剩余4条评论

1

递归互斥锁是按设计针对线程的(同一线程再次锁定 = 递归,另一个线程同时锁定 = 阻塞)。常规互斥锁没有这种设计,因此它们实际上可以在不同的线程中被锁定和解锁。


你说常规互斥锁可以被不同的线程锁定和解锁。这是否取决于实现? - jasonline
你确定你不是在指二进制信号量用作锁吗?互斥锁是被拥有的,只能由锁定它们的线程解锁。而人们习惯于“获取和释放”信号量并将其用作锁。 - D.Shawley
@jasonline:这绝对取决于具体实现。在错误的线程中解锁互斥锁始终会导致错误或未定义行为,这取决于互斥锁类型。这从来不是一个有效的操作。 - R.. GitHub STOP HELPING ICE

1

我认为这涵盖了你所有的问题。直接从Linux pthreads手册中来:

如果互斥锁类型为PTHREAD_MUTEX_NORMAL,则不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果线程试图解锁它没有锁定或解锁的互斥锁,将导致未定义行为。
如果互斥锁类型为PTHREAD_MUTEX_ERRORCHECK,则应提供错误检查。如果线程尝试重新锁定已经锁定的互斥锁,则会返回错误。 如果线程试图解锁它没有锁定或解锁的互斥锁,将返回错误。 如果互斥锁类型为PTHREAD_MUTEX_RECURSIVE,则互斥锁应维护锁计数的概念。当线程成功第一次获取互斥锁时,锁计数将设置为1。每次线程重新锁定此互斥锁时,锁计数将增加1。每次线程解锁互斥锁时,锁计数将减少1。当锁计数达到零时,互斥锁将变为可用于其他线程获取。 如果线程试图解锁它没有锁定或解锁的互斥锁,将返回错误。 如果互斥锁类型为PTHREAD_MUTEX_DEFAULT,则尝试递归锁定该互斥锁会导致未定义行为。如果调用线程未锁定互斥锁,则尝试解锁该互斥锁会导致未定义行为。 如果互斥锁未被锁定,则尝试解锁它会导致未定义行为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接