在我看来,大多数反对递归锁的论点(在我进行了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() {
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);
}
一旦事件或类似结构(访问者?!)出现,我不希望通过非递归锁来解决所有随之而来的设计问题。