【发布时间】:2015-08-21 20:33:16
【问题描述】:
有一个流行的自旋锁互斥版本,它在 Internet 上传播,人们可能会在 Anthony Williams 的书(C++ Concurrency in Action)中遇到它。这里是:
class SpinLock
{
std::atomic_flag locked;
public:
SpinLock() :
locked{ATOMIC_FLAG_INIT}
{
}
void lock()
{
while(locked.test_and_set(std::memory_order_acquire));
}
void unlock()
{
locked.clear(std::memory_order_release);
}
};
我不明白的是为什么每个人都将std::memory_order_acquire 用于test_and_set,这是一个RMW 操作。为什么不是std::memory_acq_rel?
假设我们有 2 个线程同时尝试获取锁:
T1: test_and_set -> ret false
T2: test_and_set -> ret false
这种情况应该是可能的,因为我们有 2 个 acquire 操作,它们之间没有形成任何 sync with 关系。是的,在我们解锁互斥体后,我们有一个release 操作,它领导了后续的release sequence,生活变得丰富多彩,每个人都很开心。但是为什么在release sequence 被带头之前是安全的呢?
由于许多人确切地提到了该实现,我认为它应该可以正常工作。那我错过了什么?
更新 1:
我完全理解操作是原子的,lock 和 unlock 之间的操作不能超出临界区。这不是问题。问题是我看不到上面的代码如何防止 2 个互斥锁同时进入临界区。为了防止这种情况发生,2 locks 之间应该有 happens before 关系。有人可以使用 C++ 标准概念向我展示代码是完全安全的吗?
更新 2:
好的,我相信我们已经接近正确答案了。我在标准中发现了以下内容:
[atomics.order] 第 11 条
原子读-修改-写操作应始终读取最后一个值 (按修改顺序)写在与相关的写之前 读-修改-写操作。
关于这个主要问题,我可以很高兴地结束这个问题,但我仍然有疑问。 in the modification order 部分呢?
标准很清楚:
[intro.multithread] 第 8 条
对特定原子对象 M 的所有修改都发生在某些 特定的总阶,称为 M 的修改阶。如果 A 和 B 是原子对象 M 的修改,而 A 发生在之前(如定义 下面)B,则 A 应按 M 的修改顺序在 B 之前, 定义如下。
因此,根据 RMW 操作具有最新写入值的条款,最新的写入操作应该发生在读取部分或 RMW 操作之前。问题中不是这种情况。对吧?
更新 3:
我越来越认为自旋锁的代码被破坏了。这是我的推理。 C++ 指定了 3 种类型的操作:
- 获取、释放、获取-释放 - 这些是同步操作。
- 放松 - 这些不是同步操作
- RMW - 这些是具有“特殊”特征的操作
让我们从 RMW 开始,看看它们有什么特别之处。首先,它们是形成release sequence 的宝贵资产,其次它们具有上面引用的特殊条款([atomics.order] 第 11 条)。我没有发现其他特别之处。
获取/释放是同步操作和release sync with acquire,因此形成happens before 关系。宽松的操作只是简单的原子操作,根本不参与修改顺序。
我们的代码中有什么?我们有一个使用获取内存语义的 RMW 操作,因此每当达到第一次解锁(释放)时,它都有两个角色:
- 与之前的
release形成sync with关系 - 参与
release sequence。 但这只有在第一个unlock完成后才是正确的。
在此之前,如果我们有 2+ 个线程同时运行我们的 lock 代码,那么我们可以同时输入 pass lock,因为 2 个 acquire 操作不会形成任何类型的关系。它们与放松操作一样无序。由于它们是无序的,我们不能使用任何关于 RMW 操作的特殊子句,因为没有 happens before 关系,因此 locked 标志没有修改顺序。
所以要么我的逻辑有缺陷,要么代码被破坏了。请知道真相的人对此发表评论。
【问题讨论】:
-
为什么while循环中没有sleep(0)。正如所写的那样,我们在旋转时根本不屈服,所以我们将继续旋转,直到我们的时间片结束。这可能会导致性能不佳,尤其是在单核机器上。
-
@doron,因为这个实现是为了展示如何使用原子而不是如何编写好的自旋锁。当然,这个实现是幼稚的,但它绝不意味着更多。顺便说一句,C++ 中有一个
yield函数,所以你不需要使用sleep(0)。
标签: c++ multithreading atomic