锁定函数层次结构

3

我目前遇到了一些关于C++并发编程的设计问题,希望你能帮助我:

假设某个函数 func 操作某个对象 obj。在这些操作期间需要持有一个锁(可能是 obj 的成员变量)。现在假设 func 在持有锁时调用子函数 func_2,而 func_2 又对已经被锁定的对象进行操作。然而,如果我还想从其他地方调用 func_2 而不持有锁怎么办?func_2 应该锁定 obj 吗?还是不应该锁定?我看到有三种可能性:

  1. 我可以向 func_2 传递一个布尔值,指示是否需要锁定。但这似乎会引入很多样板代码。
  2. 我可以使用递归锁,并在 func_2 中始终锁定 obj。但递归锁似乎存在问题,详情见此处
  3. 我可以假设每个调用 func_2 的函数都已经持有锁。我需要记录这一点,并可能在调试模式下强制执行此操作。从设计角度来看,函数是否应该锁定 Obj,哪些函数应该假设它已经被锁定?(显然,如果一个函数假设某些锁已经被持有,则它只能调用那些至少具有同等强度假设的函数,除此之外呢?)

我的问题是:实践中使用这些方法中的哪一个,以及为什么?

提前感谢

hfhc2

2个回答

1

好的,作为后续跟进。我最近阅读了glib的API文档,特别是关于消息传递队列的部分。我发现大多数在这些队列上操作的函数都有两个变体,分别命名为functionfunction_unlocked。这样做的想法是,如果程序员想执行单个操作,比如从队列中弹出,可以使用g_async_queue_pop()。该函数自动处理队列的锁定/解锁。然而,如果程序员想要连续弹出两个元素,可以使用以下序列:

GAsyncQueue *queue = g_async_queue_new();

// ...

g_async_queue_lock(queue);

g_async_queue_pop_unlocked(queue);
g_async_queue_pop_unlocked(queue);

g_async_queue_unlock(queue);

这类似于我的第三种方法。同样需要做出关于某些锁状态的假设,这些假设是API所需的,并且需要进行文档记录。

1

1. 传递一个指示是否锁定的指示器:

你将锁定选择权交给调用者。这是容易出错的:

  • 调用者可能会做出错误的选择
  • 调用者需要知道您对象的实现细节,从而破坏了封装原则
  • 调用者需要访问互斥锁
  • 如果您有多个对象,最终会为死锁条件提供便利

2. 递归锁:

您已经强调了这个问题。

3. 将锁定责任传递给调用者:

在您提出的不同替代方案中,这似乎是最一致的。与1相反,您不给出选择,而是完全将锁定责任传递给调用者。这是使用func_2的合同的一部分。

您甚至可以断言对象上是否设置了锁定,以防止错误(尽管检查将受到限制,因为您不一定能够验证谁拥有锁定)。

4. 重新考虑您的设计:

如果你需要确保在func_2中对象已被锁定,这意味着你有一个关键部分需要保护。两个函数都有可能需要锁定,因为它们对obj执行某些较低级别的操作,并且需要防止在对象处于不稳定状态时发生数据竞争。
我强烈建议检查是否可以将这些较低级别的程序从func和func_2中提取出来,并将它们封装在更简单的原始函数中。这种方法也可以增加短序列的锁定机会,从而增加真正并发的机会。

仅作为补充:我有时会在观察者模式中使用回调,例如每当数据发生更改时。在回调期间,我希望对象处于一致的状态,这需要保持锁定状态... - hfhc2
非常好的问题!观察者将在修改线程内调用。因此,您需要管理观察者的并发性(使用单独的数据结构和不同的锁),这是最有可能引起并发瓶颈甚至更糟的死锁的最佳选择。建议:拥有异步管理的观察者(减少瓶颈情况)?或者制作所需值的一致副本,释放锁并将值传递给观察者? - Christophe

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