锁定相互引用的资源对
Posted
技术标签:
【中文标题】锁定相互引用的资源对【英文标题】:Locking pairs of mutually referencing resources 【发布时间】:2017-05-16 21:03:37 【问题描述】:考虑以下几点:
// 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> lockmutex;
if (buddy)
std::unique_lock<std::mutex> buddyLockbuddy->mutex;
buddy->buddy = ;
当两个伙伴几乎同时被销毁时,他们每个人都可能先锁定自己的互斥锁,然后再尝试锁定他们伙伴的互斥锁,从而导致死锁。
比赛条件:
好的,所以我们只需手动或使用std::lock
以一致的顺序锁定互斥锁:
~Guy()
std::unique_lock<std::mutex> lockmutex, std::defer_lock;
if (buddy)
std::unique_lock<std::mutex> buddyLockbuddy->mutex, std::defer_lock;
std::lock(lock, buddyLock);
buddy->buddy = ;
不幸的是,要访问好友的互斥锁,我们必须访问buddy
,此时它不受任何锁的保护,并且可能正在从另一个线程修改,这是一个竞争条件。
不可扩展:
正确性可以通过全局互斥锁实现:
static std::mutex mutex;
~Guy()
std::unique_lock<std::mutex> lockmutex;
if (buddy) buddy->buddy = ;
但出于性能和可扩展性的原因,这是不可取的。
那么这是否可以在没有全局锁定的情况下完成?怎么样?
【问题讨论】:
我认为您需要将“buddyness”和“guyness”的关注点分开。即一个名为“Guys”的新类,其中包含对零个、一个或两个人的引用。 @RichardHodges 这有什么帮助?Guys
和每个 Guy
之间也会存在同样的问题。
我有点同意@RichardHodges 的观点。从概念上讲,似乎需要同步的是Guy
s 之间的关系,而不是Guy
s 本身。这可以通过在Guy
s 之间共享的互斥锁来完成,只要好友关系被切断(新的Guy
成为朋友或销毁),该互斥锁就会被锁定。
我真的很喜欢这个挑战。我对“哲学家进餐”问题持批评态度,因为虽然它展示了死锁,但它并不是典型系统的非常现实的模型,因为锁的数量与线程的数量相同,这是一个非典型问题。我想知道应用程序可能是什么。我能想到的只是一群工人——生男孩的助产士,与他们交朋友和解除朋友关系的媒人,以及随机杀死他们的收割者。
@Persixty,实际的应用程序是一对future/promise-like对象,与std
对应的对象不同,它们不进行动态内存分配,因此不能通过共享状态实现。 befriend
是一种类似于pipe
的操作,它将两对promiseA->futureA
和promiseB->futureB
变成一对promiseA->futureB
。
【参考方案1】:
使用std::lock
不是竞争条件(本身),也不会冒死锁的风险。
std::lock
将使用无死锁算法来获得这两个锁。它们将是某种(未指定)尝试和撤退的方法。
另一种方法是确定任意锁定顺序,例如使用对象的物理地址。
您已经正确排除了对象好友本身的可能性,因此没有尝试两次lock()
相同的mutex
的风险。
我说这本身不是竞争条件,因为该代码将确保完整性,即如果 a 有伙伴 b,则 b 对所有 a 和 b 都有伙伴 a。
在与两个对象成为朋友之后,他们可能会与另一个线程解除朋友关系这一事实大概是您打算或通过其他同步解决的问题。
还请注意,当您与新朋友的朋友成为朋友并且可能取消朋友的朋友时,您需要立即锁定所有对象。 那是两个“成为”朋友和他们现在的朋友(如果有的话)。 因此,您需要锁定 2,3 或 4 个互斥锁。
std::lock
不幸的是不使用数组,但有一个版本可以在 boost 中执行此操作,或者您需要手动解决它。
为了澄清,我正在阅读可能的析构函数作为模型的示例。所有相关成员都需要对相同的锁进行同步(例如,befriend()
、swap()
和 unfriend()
,如果需要)。事实上,锁定 2,3 或 4 的问题适用于 befriend()
成员。
此外,析构函数可能是最糟糕的例子,因为正如评论中提到的,一个对象是可破坏的但可能与另一个线程发生锁争用是不合逻辑的。在更广泛的程序中肯定需要存在一些同步,以使这种同步成为不可能,此时析构函数中的锁是多余的。
确实,确保Guy
对象在销毁之前 没有伙伴的设计似乎是一个好主意,并且是在析构函数中检查assert(buddy==nullptr)
的调试前提条件。遗憾的是,这不能作为运行时异常留下,因为在析构函数中抛出异常会导致程序终止 (std::terminate()
)。
事实上,真正的挑战(可能取决于周围的程序)是如何在交友时解除交友。这似乎需要一个 try-retreat 循环:
-
锁定 a 和 b。
看看他们有没有朋友。
如果他们已经是好友 - 你就完了。
如果他们有其他好友,解锁 a 和 b,并锁定 a 和 b 及其好友(如果有)。
如果他们再次去,请检查伙伴是否没有改变。
调整相关成员。
是否存在活锁风险但任何尝试撤消方法都存在相同风险,这是周边程序的问题。
不行的是std::lock()
a & b 然后std::lock()
伙计们,因为这确实有死锁的风险。
所以回答这个问题 - 是的,没有全局锁是可能的,但这取决于周围的程序。它可能在许多Guy
对象的群体中争用很少且活跃度很高。
但可能是少数对象激烈竞争(可能在大量人群中)导致问题。如果不了解更广泛的应用,就无法评估。
解决这个问题的一种方法是锁升级,它实际上是零碎地回落到全局锁。从本质上讲,这意味着如果重试循环的行程过多,则会设置一个全局信号量,命令所有线程进入全局锁定模式一段时间。一段时间可能是一些操作或一段时间,或者直到对全局锁的争用平息!
所以最终的答案是“是的,除非它不起作用,否则绝对有可能”。
【讨论】:
再次仔细阅读#2。问题是如果不锁定第一个互斥锁,我就无法安全地到达第二个互斥锁。锁定第一个互斥锁会导致 #1 出现死锁,并且不会锁定它 - 在竞争条件下,buddy
指针仍然不受保护。
@yurikilochek 仅当std::lock
损坏时! std::lock
使用死锁避免算法。这就是它的目的。确保一次性锁定所需的一切!注意:虽然你的析构函数令人担忧。如果您触发了析构函数,我认为您已经(通过其他同步方式)确保没有其他线程可以访问正在销毁的对象。但这并不(必然)适用于它的buddy
。 en.cppreference.com/w/cpp/thread/lock
在算法的第 4 步有一个竞争条件:一旦 a&b 在获得指向其好友的指针后解锁,好友可能会被销毁,因此它会尝试锁定不存在的互斥锁。
@yurikilochek 正如我在文中提到的,问题在于生命周期。您需要单独处理生命周期。如果两个线程争用一个对象并且一个线程破坏了它,那么另一个线程将失败。析构函数中的互斥锁使其线程安全的想法从根本上是有缺陷的。您需要确保当一个对象被破坏时,只有一个线程引用它。这不是该步骤的问题,而是您需要解决的整体设计问题。但如果不知道您的应用程序是做什么的,我无法给出答案...
@yurikilochek 我现在已经阅读了你的另一条笔记。如果对象不是动态的并且在堆栈上,您需要确保线程没有引用它们或它们本身已停止(使用join()
),然后才能到达作用域末尾的Guy
析构函数。如果在另一个线程正在等待它时销毁互斥锁,则程序将失败。您无法使用对象中的互斥锁来保护析构函数。以上是关于锁定相互引用的资源对的主要内容,如果未能解决你的问题,请参考以下文章
C ++,Qt - 锁定保护和返回对对象的不可分配引用的安全性