恕我直言,大多数反对递归锁的论点(在 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() }
}
一旦事件或类似构造(访问者?!)开始发挥作用,我不希望通过一些非递归锁来解决所有随之而来的设计问题。