c++ - 如果atomic_compare_exchange在它自己的线程上不是原子的,它如何实现锁?

标签 c++ multithreading atomic

如果我有

std::atomic<uint64_t> guard;
// ...
if (std::atomic_compare_exchange_strong(
        &guard, &kExpected, kValue,
        std::memory_order_acquire, std::memory_order_relaxed)) {
   int foo = *non_atomic_thing;
   // ...
}

我知道在读取guard之前不能重新排序non_atomic_thing的读取。但是否有可能在写入之前重新排序?也就是说,如果其他线程使用 std::memory_order_acquire 读取 guard,观察到它不等于 kValue,并写入它,则为这是一场数据竞赛?

(cppreference.com对此不是很清楚,但是Rust的文档具有相同的语义,具体说acquire仅适用于加载部分,release仅适用于存储: "Using Acquire as success ordering makes the store part of this operation Relaxed" )

如果这是真的,我如何告诉其他线程在我读取它时不要覆盖我的值?

最佳答案

C++ 标准在这一点上有点模糊。但他们可能的意图以及许多实现中实际发生的情况是,您的非原子读取可以在写入之前重新排序。请参阅For purposes of ordering, is atomic read-modify-write one operation or two? .

但是,对于通常的锁定习惯,这不是问题,并且不会导致数据争用。在获取锁时,很容易将基本操作视为“锁定”值的存储,这告诉其他线程该锁是我们的,他们不能拥有它。但实际上,重要的是“解锁”值的负载。这证明该锁属于我们的线程。读取-修改-写入操作的原子性保证在我们重置该值之前没有其他线程会加载该值,因此我们可以安全地进入临界区,即使我们存储的“锁定”值直到一段时间后。

你可以想象这发生在LL/SC architecture上。假设加载链接完成并返回“解锁”值。条件存储尚未完成,如果其他核心访问了锁变量并且我们失去了它的独占所有权,它可能会失败。然而,与此同时,我们可以对非原子变量执行推测性加载,前提是除非 SC 成功,否则它不会退出。如果 SC 失败,则必须重做 LL/SC 对,在这种情况下,我们的非原子加载也将重做。因此,当我们最终获得成功时,非原子加载将在 LL 之后可见,但可能在 SC 之前可见。

要了解这是如何遵循 C++ 内存排序规则的,让我们考虑一个更具体的示例:

std::atomic<int> lock{0};
int non_atomic;

void reader() {
    int expected = 0;
    if (lock.compare_exchange_strong(expected, 1,
            std::memory_order_acquire, std::memory_order_relaxed)) {
        int local = non_atomic;
        // ...
        lock.store(0, std::memory_order_release);
    }
}

void writer() {
    int expected = 0;
    if (lock.compare_exchange_strong(expected, 2,
            std::memory_order_acquire, std::memory_order_relaxed)) {
        non_atomic = 42;
        // ...
        lock.store(0, std::memory_order_release);
    }
}

读-修改-写操作的原子性 promise 来自 C++20 [atomics.order p10]:

Atomic read-modify-write operations shall always read the last value (in the modification order) written before the write associated with the read-modify-write operation.

lock的修改顺序是一个总订单。仅当两个比较交换都读取值 0 时才会进入临界区。 reader 加载的 0在修改顺序中必须紧跟 1,并且 0 由 writer 加载。后面必须紧跟 2。因此 0 必须出现两次。 lock的修改顺序因此必须是 0 1 0 2 00 2 0 1 0 .

如果是0 1 0 2 0 ,那么第二个 0 必须来自 reader 中的发布存储。 。它不可能来自 writer 的商店,因为按照修改顺序,该 1 必须位于 2 之后(因为 writer 的 0 存储在其 2 的存储之后排序;这是写-写一致性,[intro.races p15])。所以获取负载在 writer读取顺序中的第二个 0,它是由发布存储放在 reader 中的。 ,因此它们同步。这确保了对 non_atomic 的访问在reader发生在 writer 中的访问之前,并且不存在数据竞争。

如果是0 2 0 1 0 ,逻辑反过来,访问writer中发生在 reader 中的访问之前;再次没有数据竞争。

关于c++ - 如果atomic_compare_exchange在它自己的线程上不是原子的,它如何实现锁?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72411265/

相关文章:

c++ - 大量 sp_counted_impl_p 对象

C++实现模板类时编译错误

MySQL:生产者/消费者模型,访问多个表的命令需要原子性

c++ - 如何在 CONST 方法中访问类似于 GCC 的 sync_fetch_and_add 的 32 位或 64 位整数值?

c++ - 将参数传递到 C++ 共享库

c++ - 用于匹配和计数的字符串和 int 的容器?

java批量生产者消费者

c# - Task.WaitAny 的重载版本是否取消任务?

c# - 尝试使用异步方法访问 Thread.CurrentPrincipal 时出现 ObjectDisposedException

python - python中主线程和子线程之间的原子操作