恕我直言,大多数反对递归锁的论点(这是我在 20 年的并发编程中 99.9% 的时间使用的)将它们的好坏与其他完全无关的软件设计问题混为一谈。举个例子,“回调”问题,在没有任何多线程相关观点的情况下详尽地阐述了这一问题,例如在Component software - beyond Object oriented programming一书中。
一旦你有一些控制反转(例如事件被触发),你就会面临重入问题。与是否涉及互斥锁和线程无关。
class EvilFoo {
std::vector<std::string> data;
std::vector<std::function<void(EvilFoo&)> > changedEventHandlers;
public:
size_t registerChangedHandler( std::function<void(EvilFoo&)> handler) { // ...
}
void unregisterChangedHandler(size_t handlerId) { // ...
}
void fireChangedEvent() {
// bad bad, even evil idea!
for( auto& handler : changedEventHandlers ) {
handler(*this);
}
}
void AddItem(const std::string& item) {
data.push_back(item);
fireChangedEvent();
}
};
现在,使用上面这样的代码,您会得到所有错误情况,这些情况通常会在递归锁的上下文中命名 - 只是没有任何错误情况。事件处理程序一旦被调用就可以取消注册,这将导致在幼稚编写的fireChangedEvent()
. EvilFoo
或者它可以调用导致各种问题的其他成员函数。根本原因是重入。最糟糕的是,这甚至可能不是很明显,因为它可能会在整个事件链中触发事件,最终我们会回到我们的 EvilFoo(非本地)。
所以,重入是根本问题,而不是递归锁。现在,如果您觉得使用非递归锁更安全,那么这样的错误会如何表现出来呢?每当发生意外的重新进入时,就会陷入僵局。并使用递归锁?同样,它会在没有任何锁的代码中体现出来。
所以邪恶的部分EvilFoo
是事件以及它们是如何实现的,而不是递归锁。fireChangedEvent()
对于初学者来说,首先需要创建一个副本changedEventHandlers
并将其用于迭代。
另一个经常进入讨论的方面是首先定义锁应该做什么:
- 保护一段代码不被重新输入
- 保护资源不被同时使用(被多个线程)。
我做并发编程的方式,我有后者的心智模型(保护资源)。这是我擅长递归锁的主要原因。如果某些(成员)函数需要锁定资源,它会锁定。如果它在执行它的操作时调用另一个(成员)函数并且该函数也需要锁定 - 它会锁定。而且我不需要“替代方法”,因为递归锁的引用计数与每个函数编写如下内容完全相同:
void EvilFoo::bar() {
auto_lock lock(this); // this->lock_holder = this->lock_if_not_already_locked_by_same_thread())
// do what we gotta do
// ~auto_lock() { if (lock_holder) unlock() }
}
一旦事件或类似的构造(访问者?!)开始发挥作用,我不希望通过一些非递归锁来解决所有随之而来的设计问题。