【问题标题】:Is this an appropriate use case for a recursive mutex?这是递归互斥锁的合适用例吗?
【发布时间】:2019-10-02 01:10:40
【问题描述】:

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

【问题讨论】:

  • 如果您不使用具有递归功能的互斥锁,您必须确保您的代码不会递归。当您处理自己的代码时,这很容易实现,但是当您导出库时,您不知道它将调用您的代码,因此提供具有递归功能的函数可能更通用。我看不到任何其他用途
  • @geckos 说得好。在这样的方法中,我很难决定是否应该由客户端来确保同步访问,或者是否应该由我提供。在这种情况下,我将使用它的代码,所以我认为拥有一个非递归互斥体就可以完成这项工作。
  • 如果做 API 我会导出两个函数,一个支持递归,另一个不支持。递归互斥锁可能比非递归互斥锁有更多的开销,但是,如果我有一个可以通过递归更好地处理的问题,我会使用递归互斥锁,直到它们被证明是一个问题。我还听说过早的优化是万恶之源,如果您的问题不是递归的,请使用非递归互斥锁:)

标签: c mutex recursive-mutex


【解决方案1】:

您的两个示例 - 原始 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] 实际上,锁定您已经拥有的互斥锁应该很便宜,但在您的示例中,它至少需要调用无法内联的外部库函数。

【讨论】:

  • 关于第 1 点,我打算问是否让数据结构管理自己的锁会提供一种避免暴露内部结构的方法,但第 2 点与该设计相冲突选择。我想我现在理解了权衡:我可以让调用线程获取锁来执行批量操作,或者让数据结构提供允许批量操作的 API,但在这两种情况下,只有非递归互斥锁是必需的。面对这种替代设计选择时,使用递归互斥锁将是矫枉过正。
  • 我想我倾向于让 API 提供批量操作,这样客户端就不必担心同步问题。无论如何,谢谢你的合理解释!
  • @Anton,如果我要提供一种从堆栈中批量弹出的方法,但我想使用弹出的元素,我可能不得不返回某种列表.这是你想象中的样子吗?
  • @asfeynman 您可以返回一个列表或传入一个函数指针,以便在弹出项目时一次处理一个项目。这真的取决于你如何使用这个数据结构。
  • @Anton,明白了。
【解决方案2】:

您描述的逻辑实际上不是递归互斥体,也不是一个合适的情况。

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

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

【讨论】:

  • “您描述的逻辑实际上不是递归互斥锁。”也许我误解了你,但create_synchronized_counter() 不是在创建递归互斥锁吗? “如果你真的需要确保另一个线程不会增加你的计数器。”目标是让主线程连续增加计数器 3 次,而不会让另一个线程在其间增加计数器。如果这没有按预期发生,您能描述一下它可能不起作用的场景吗?
  • 关于您的第二段,请参阅编辑。
猜你喜欢
  • 1970-01-01
  • 2010-09-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-29
  • 2020-08-04
  • 1970-01-01
相关资源
最近更新 更多