4

我从各种来源( 12 )听说应该避免使用递归互斥锁,因为这可能是黑客或糟糕设计的标志。但是,有时我认为它们可能是必要的。鉴于此,以下是递归互斥锁的合适用例吗?

// 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 个元素,但它必须一次全部完成(中间没有另一个线程从堆栈中压入或弹出)。设计问题的关键是我是否应该让客户端自己管理使用非递归互斥锁锁定堆栈,或者让堆栈提供同步的、简单的方法以及递归互斥锁,客户端可以使用这些方法进行多个原子事务也是同步的。

4

2 回答 2

4

您的两个示例(原始示例synchronized_counter和编辑中的堆栈)都是使用递归互斥锁的正确示例,但如果您正在构建数据结构,它们将被视为糟糕的 API 设计。我将尝试解释原因。

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

  2. 效率- 实施专门的批量操作(如increment_by(n)或)通常更有效pop_many(n)

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

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

  • 我有一个有两种方法FooBar.
  • 该类被设计为单线程。
  • 有时Foo来电Bar

我想让类线程安全,所以我在类中添加了一个互斥锁并将其锁定FooBar. 现在我需要确保Bar在调用时可以锁定互斥锁Foo

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

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

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


[1] 有一些诡计模式,例如“弹出一个项目,查看它,决定是否弹出另一个”,但这些可以通过为批量操作提供谓词来实现。

[2] 实际上,锁定您已经拥有的互斥锁应该很便宜,但在您的示例中,它至少需要调用无法内联的外部库函数。

于 2019-10-02T02:51:34.643 回答
-1

您描述的逻辑实际上不是递归互斥锁,也不是一个合适的案例。

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

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

于 2019-10-02T01:25:35.237 回答