不同两个线程中`compare_exchange_strong`中的读操作能否读取到相同的值?

问题描述 投票:0回答:1

[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,标准中的正式措辞并不禁止这种可能性。如果不能的话,哪里有相关的措辞表明这是不可能的?

c++ language-lawyer c++20 atomic
1个回答
1
投票

我认为您担心两个

CAS_strong
操作实际上都可以在比较中成功并且都获得锁定。这是不可能发生的,因为根据 [atomics.ref.ops] #19,任何
compare_exchange
成功时都是原子 RMW 操作:

如果操作返回

true
,则这些操作是针对
*ptr
引用的值的原子读取-修改-写入操作。 否则,这些操作是该内存上的原子加载操作。

[atomics.order] #10 适用:

原子读-修改-写操作应始终读取在与读-修改-写操作关联的写操作之前写入的最后一个值(按修改顺序)。

这就是跨线程序列化单个对象上的原子 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。

© www.soinside.com 2019 - 2024. All rights reserved.