请考虑以下事项:
// There are guys:
class Guy {
// Each guy can have a buddy:
Guy* buddy; // When a guy has a buddy, he is his buddy's buddy, i.e:
// assert(!buddy || buddy->buddy == this);
public:
// When guys are birthed into the world they have no buddy:
Guy()
: buddy{}
{}
// But guys can befriend each other:
friend void befriend(Guy& a, Guy& b) {
// Except themselves:
assert(&a != &b);
// Their old buddies (if any), lose their buddies:
if (a.buddy) { a.buddy->buddy = {}; }
if (b.buddy) { b.buddy->buddy = {}; }
a.buddy = &b;
b.buddy = &a;
}
// When a guy moves around, he keeps track of his buddy
// and lets his buddy keep track of him:
friend void swap(Guy& a, Guy& b) {
std::swap(a.buddy, b.buddy);
if (a.buddy) { a.buddy->buddy = &a; }
if (b.buddy) { b.buddy->buddy = &b; }
}
Guy(Guy&& guy)
: Guy()
{
swap(*this, guy);
}
Guy& operator=(Guy guy) {
swap(*this, guy);
return *this;
}
// When a Guy dies, his buddy loses his buddy.
~Guy() {
if (buddy) { buddy->buddy = {}; }
}
};
到目前为止一切都很顺利,但现在我希望这可以在不同的线程中使用好友时使用。没问题,让我们在std::mutex
中坚持Guy
:
class Guy {
std::mutex mutex;
// same as above...
};
现在,我必须在链接或取消链接这两者之前锁定两个人的互斥锁。
这是我难倒的地方。以下是失败的尝试(以析构函数为例):
死锁:
~Guy() {
std::unique_lock<std::mutex> lock{mutex};
if (buddy) {
std::unique_lock<std::mutex> buddyLock{buddy->mutex};
buddy->buddy = {};
}
}
当两个好友大约在同一时间被摧毁时,他们每个人都可能会锁定他们自己的互斥锁,然后才能锁定他们的好友。互斥,从而导致僵局。
比赛条件:
好的,我们只需要按照一致的顺序锁定互斥锁,无论是手动还是std::lock
:
~Guy() {
std::unique_lock<std::mutex> lock{mutex, std::defer_lock};
if (buddy) {
std::unique_lock<std::mutex> buddyLock{buddy->mutex, std::defer_lock};
std::lock(lock, buddyLock);
buddy->buddy = {};
}
}
不幸的是,为了获得好友的互斥锁,我们必须访问buddy
,此时此版本不受任何锁的保护,并且可能正在从另一个线程进行修改,这是一场比赛条件。
不可扩展:
使用全局互斥锁可以 。
static std::mutex mutex;
~Guy() {
std::unique_lock<std::mutex> lock{mutex};
if (buddy) { buddy->buddy = {}; }
}
但出于性能和可扩展性的原因,这是不可取的。
这可能没有全局锁定吗?怎么样?
答案 0 :(得分:2)
使用std::lock
不是竞争条件(本身)也不存在死锁风险。
std::lock
将使用无死锁算法来获取两个锁。它们将是某种(未指明的)尝试和撤退方法。
另一种方法是确定任意锁定顺序,例如使用对象的物理地址。
您已经正确地排除了对象伙伴本身的可能性,因此没有尝试lock()
同一mutex
两次的风险。
我说这不是一个竞争条件本身,因为该代码将做的是确保完整性,如果有一个伙伴b,那么b为所有a和b都有伙伴a。
事实上,在与两个对象建立联系之后,他们可能会被另一个线程感到不友好,这可能是您打算或通过其他同步解决的问题。
另请注意,当您与朋友结交并且可能与新朋友的朋友不友好时,您需要立即锁定所有对象。 这是两个&#39;朋友和他们现在的朋友(如果有的话)。 因此,您需要锁定2,3或4个互斥锁。
std::lock
遗憾的是没有接受数组,但有一个版本可以在boost中执行,或者您需要手动解决它。
为了澄清,我正在阅读可能的析构函数的示例作为模型。所有相关成员都需要对同一锁定进行同步(例如befriend()
,swap()
和unfriend()
(如果需要)。实际上,锁定2,3或4的问题适用于befriend()
成员。
此外,析构函数可能是最糟糕的例子,因为正如评论中所提到的那样,一个对象是可破坏的,但可能与另一个线程发生锁争用是不合逻辑的。在更广泛的程序中肯定需要存在一些同步,以使得不可能在这一点上析构函数中的锁是多余的。
确实在销毁之前确保Guy
个对象没有伙伴的设计似乎是一个好主意和一个在析构函数中检查assert(buddy==nullptr)
的调试前置条件。遗憾的是,它不能作为运行时异常,因为在析构函数中抛出异常会导致程序终止(std::terminate()
)。
事实上,真正的挑战(可能取决于周围的计划)是如何在交友时取消联系。这似乎需要一个尝试撤退循环:
对于周围的程序来说,这是一个存在实时锁定风险的问题,但任何尝试撤退方法都会遭受同样的风险。
std::lock()
a&amp; b然后std::lock()
好友,因为这确实存在死锁风险。
所以回答这个问题 - 是的,没有全局锁定是可能的,但这取决于周围的程序。它可能在许多Guy
个对象的群体中,争用很少,而且活力很高。
但可能是有少量对象热议(可能在大量人群中)导致问题。如果不了解更广泛的应用,就无法对其进行评估。
解决这个问题的一种方法是锁定升级,这实际上是零碎的回归到全局锁定。本质上,这意味着如果在重试循环周围有太多的行程,则将设置全局信号量,命令所有线程进入全局锁定模式一段时间。一段时间可能是一些行动或一段时间或直到全球锁定的争论消退!
所以最后的答案是&#34;是的,它是绝对可能的,除非它在哪种情况下都不起作用&#39; no&#39;&#34;。