这是递归互斥锁的合适用例吗?

Is this an appropriate use case for a recursive mutex?

我从各种渠道听说过 (1, 2) 人们应该避免使用递归互斥锁,因为它可能是黑客攻击的标志或 糟糕的设计。然而,有时我认为它们可能是必要的。有鉴于此, 以下是递归互斥锁的合适用例吗?

// main.c
// gcc -Wall -Wextra -Wpedantic main.c -pthread

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif /* _GNU_SOURCE */

#include <assert.h>
#include <pthread.h>
#include <stdlib.h>

typedef struct synchronized_counter
{
    int count;
    pthread_mutex_t mutex;
    pthread_mutexattr_t mutexattr;
} synchronized_counter;

synchronized_counter* create_synchronized_counter()
{
    synchronized_counter* sc_ptr = malloc(sizeof(synchronized_counter));
    assert(sc_ptr != NULL);

    sc_ptr->count = 0;

    assert(pthread_mutexattr_init(&sc_ptr->mutexattr) == 0);
    assert(pthread_mutexattr_settype(&sc_ptr->mutexattr, 
        PTHREAD_MUTEX_RECURSIVE) == 0);

    assert(pthread_mutex_init(&sc_ptr->mutex, &sc_ptr->mutexattr) == 0);

    return sc_ptr;
}

void synchronized_increment(synchronized_counter* sc_ptr)
{
    assert(pthread_mutex_lock(&sc_ptr->mutex) == 0);

    sc_ptr->count++;

    assert(pthread_mutex_unlock(&sc_ptr->mutex) == 0);
}

int main()
{
    synchronized_counter* sc_ptr = create_synchronized_counter();

    // I need to increment this counter three times in succesion without having
    // another thread increment it in between. Therefore, I acquire a lock
    // before beginning.

    assert(pthread_mutex_lock(&sc_ptr->mutex) == 0);

    synchronized_increment(sc_ptr);
    synchronized_increment(sc_ptr);
    synchronized_increment(sc_ptr);

    assert(pthread_mutex_unlock(&sc_ptr->mutex) == 0);

    return 0;
}

编辑:

我想用一个简单的例子来问这个问题,但可能 简单了。这是我想象的更现实的场景:我有一个堆栈数据结构,将被多个线程访问。特别是,有时一个线程将从堆栈中弹出 n 个元素,但它必须一次全部完成(中间没有另一个线程从堆栈中弹出或弹出)。设计问题的症结在于,我是否应该让客户端管理自己使用非递归互斥锁锁定堆栈,或者让堆栈提供同步的简单方法以及客户端可以用来进行多个原子事务的递归互斥锁也同步了。

您描述的逻辑实际上不是递归互斥体,也不适合用于递归互斥体。

而且,如果您确实需要确保另一个线程不会增加您的计数器,很遗憾地告诉您,您编写的逻辑无法确保这一点。

因此,我建议您退后一步,清醒头脑,重新考虑您的实际用例。我认为对递归互斥体的混淆使您误入歧途。您现在在 synchronized_increment 中拥有的逻辑很可能是这种情况……事实上,整个方法的需要……是不必要的,而您在 main 中显示的逻辑是所有你真正需要的,毕竟它只是一个简单的变量。

您的两个示例 - 原始 synchronized_counter 和您编辑的堆栈 - 都是使用递归互斥锁的正确示例,但如果您构建数据结构。我会尝试解释原因。

  1. 暴露内部结构 - 调用者需要使用相同的锁来保护对数据结构成员的内部访问。这打开了将锁滥用于访问数据结构之外的目的的可能性。这可能会导致锁争用 - 或者更糟 - 死锁。

  2. 效率 - 实施像 increment_by(n)pop_many(n) 这样的专门批量操作通常效率更高。

    • 首先,它允许数据结构优化操作——也许计数器可以只做 count += n 或者堆栈可以在一次操作中从链表中删除 n 项。 [1]
    • 其次,您不必为每个操作 lock/unlock 互斥锁来节省时间。[2]

也许使用递归互斥锁的更好示例如下:

  • 我有一个 class,有两种方法 FooBar
  • class 设计为单线程。
  • 有时 Foo 调用 Bar

我想让 class 成为线程安全的,所以我向 class 添加了一个互斥量并将其锁定在 FooBar 中。现在我需要确保 Bar 在从 Foo.

调用时可以锁定互斥锁

在没有递归互斥量的情况下解决这个问题的一种方法是创建一个私有 unsynchronized_bar 并在锁定互斥量后让 FooBar 调用它。

如果 Foo 是一个可以由子 class 实现并用于调用 Bar 的虚方法,或者如果 Foo 调用输出到可以回调到 Bar 的程序的其他部分。但是,如果你有临界区内的代码(受互斥锁保护的代码)调用其他任意代码,那么程序的行为将很难理解,并且很容易导致不同线程之间的死锁,即使你使用递归互斥。

最好的建议是通过良好的设计而不是花哨的同步原语来解决并发问题。


[1] 有一些像 "pop an item, look at it, decide if I pop another one" 这样的欺骗模式,但可以通过为批量操作提供谓词来实现这些模式。

[2] 实际上锁定一个你已经拥有的互斥量应该是非常便宜的,但在你的例子中它至少需要调用一个外部库函数,它不能被内联。