[atomics.types.operations] p23 说:
boolcompare_exchange_weak(T&预期,T预期,内存顺序成功,内存顺序失败)noexcept;
作用:获取预期值。 然后,它以原子方式将 this 指向的值的值表示形式与之前从预期中检索到的值进行比较,如果为 true,则将 this 指向的值替换为所需的值。 当且仅当比较为真时,内存根据成功的值受到影响,如果比较为假,则根据失败的值影响内存。 当仅提供一个 memory_order 参数时,成功的值为 order,失败的值为 order,但 Memory_order::acq_rel 的值应替换为 value memory_order::acquire 和 memory_order 的值::release 应替换为值 memory_order::relaxed。 当且仅当比较为 false 时,在原子操作之后,expected 中的值将被替换为原子比较期间 this 指向的值。如果操作返回 true,则这些操作是对此指向的内存的原子读取-修改-写入操作 ([intro.multithread])。否则,这些操作是该内存上的原子加载操作。
考虑这个例子:
#include <iostream>
#include <atomic>
#include <thread>
struct SpinLock{
std::atomic<bool> atomic_;
void lock(){
bool expected = false;
while (!atomic_.compare_exchange_strong(expected,true,std::memory_order_release,std::memory_order_relaxed)){
}
}
void unlock(){
atomic_.store(false, std::memory_order_release);
}
};
int main(){
SpinLock spin{false};
auto t1 = std::thread([&](){
spin.lock();
spin.unlock();
});
auto t2 = std::thread([&](){
spin.lock();
spin.unlock();
});
t1.join();
t2.join();
}
在这个例子中,
spin
被初始化为值false
,那么,两个线程中的比较是否可以同时读取这个初始值false
并与期望值false
进行同等比较? IMO,标准中的正式措辞并不禁止这种可能性。如果不能的话,哪里有相关的措辞表明这是不可能的?
我认为您担心两个
CAS_strong
操作实际上都可以在比较中成功并且都获得锁定。这是不可能发生的,因为根据 [atomics.ref.ops] #19,任何 compare_exchange
成功时都是原子 RMW 操作:
如果操作返回
,则这些操作是针对true
引用的值的原子读取-修改-写入操作。 否则,这些操作是该内存上的原子加载操作。*ptr
原子读-修改-写操作应始终读取在与读-修改-写操作关联的写操作之前写入的最后一个值(按修改顺序)。
这就是跨线程序列化单个对象上的原子 RMW,同样的规则意味着 1000 次
.fetch_add(1)
操作会将值总共增加 1000,而不丢失计数。 (这是一个常见的误解,认为“最后值”保证除此之外还有用,例如提供较低的线程间延迟或其他东西。但事实并非如此。)
如果两个线程加载相同的值,然后都根据它们加载的值存储一个值,则第二个存储将不会基于 RMW 的“写入之前写入的最后一个值(按 mod 顺序)”部分,因为其他线程刚刚存储了一个按修改顺序位于此存储之前的值。因此,它将基于修改顺序中较旧的负载,从而违反了该规则。这条规则保证了 RMW 的原子性。
但是您的标题问题的答案是“是”,只要至少有一个
CAS_strong
操作的 expected
与加载的值不匹配。
在 CAS 失败时,它在形式上只是一个加载,而不是存储回现有值的 RMW(尽管在 x86 上的实践中会发生这种情况),因此原子 RMW 的“最新值”措辞并不禁止来自的两个 CAS_strong 操作基于在对象的修改顺序中看到相同的值而返回
false
。
它甚至可以发生在真实的硬件上,至少在 LL/SC 机器上,其中 CAS_strong 只是加载链接/存储条件的循环。
我认为 LL 可以在没有缓存行独占所有权的情况下产生一个值。或者,即使在负载发生之前它确实需要独占所有权,它也可能在 SC 之前失去所有权。或者由于比较错误,甚至无法到达 SC。
(如果比较成功,但核心在达到存储条件之前丢失了缓存行,CAS_strong 将重试。CAS_weak 将返回 false,即“虚假”失败。)
我不认为 LL 需要独家所有权。 SC 在真正提交之前就已经完成了,因此它的成功/失败取决于获得独占所有权。但是知道没有其他线程修改我们的 LL 和 SC 之间的值仅取决于没有缓存行无效。我认为在一段时间内处于共享状态是可以的,尽管这确实让其他核心在我们的负载和存储之间读取它。因此,如果我们想讨论所有内核对该对象的加载/存储顺序,其中加载/存储部分之间没有任何内容,那么 LL 会在我们使其他副本无效时(当我们获得独占所有权时)有效地读取该行。这个原子 RMW。