【问题标题】:fetch_add with acq_rel memory orderfetch_add 与 acq_rel 内存顺序
【发布时间】:2017-03-31 15:40:38
【问题描述】:

考虑一个

std::atomic<int> x(0);

假设我有一个执行以下操作的函数:

int x_old = x.fetch_add(1,std::memory_order_acq_rel);

基于description for acquire release memory ordering

memory_order_relaxed 宽松操作:没有同步或排序约束,此操作只需要原子性(请参阅下面的宽松排序)

memory_order_consume 使用此内存顺序的加载操作对受影响的内存位置执行消耗操作:当前线程中依赖于当前加载的值的读取或写入不能在此加载之前重新排序。在释放相同原子变量的其他线程中写入数据相关变量在当前线程中是可见的。在大多数平台上,这只会影响编译器优化(请参阅下面的发布-使用顺序)

memory_order_acquire 使用此内存顺序的加载操作会在受影响的内存位置上执行获取操作:在此加载之前,当前线程中的任何读取或写入都不能重新排序。释放相同原子变量的其他线程中的所有写入在当前线程中都是可见的(请参阅下面的 Release-Acquire 顺序)

memory_order_release 具有此内存顺序的存储操作执行释放操作:当前线程中的任何读取或写入都不能在此存储之后重新排序。当前线程中的所有写入在获取相同原子变量的其他线程中都是可见的(请参阅下面的 Release-Acquire 排序),并且将依赖项携带到原子变量中的写入在使用相同原子的其他线程中变得可见(请参阅 Release-Consume在下面订购)。

memory_order_acq_rel 具有这种内存顺序的读-修改-写操作既是获取操作又是释放操作。当前线程中的内存读取或写入不能在此存储之前或之后重新排序。释放相同原子变量的其他线程中的所有写入在修改之前都是可见的,并且修改在获取相同原子变量的其他线程中是可见的。

memory_order_seq_cst 任何具有此内存顺序的操作既是获取操作又是释放操作,另外存在一个总顺序,其中所有线程以相同顺序观察所有修改(请参阅顺序一致的顺序下面)

是否有可能 2 个不同的线程接收相同的 x_old 值 0?或者它们是否保证以 x_old 仅对其中一个为 0,而对另一个为 1 的方式执行。

如果x_old 对它们都可能为0,那么将内存顺序更改为std::memory_order_seq_cst 是否保证x_old 的唯一性?

【问题讨论】:

  • 内存排序不相关。两个线程不可能获得相同的值(假设表达式在每个线程中只到达一次)。
  • 为什么内存排序不重要?难道两个线程都不能在 acq_rel 下获得相同的 x_old,因为没有保证读-修改-写操作的读-读顺序?
  • 排序在这里根本不相关。从 fetch_add 返回的前两个值将始终为:0 和 1,但不能保证哪个线程获得哪个值。无论您选择哪种 memory_order 都是如此。
  • 在这种情况下,fetch_add 操作上的 seq_cst 和 acq_rel memory_ordering 究竟有什么区别?
  • 使用 seq_cst 的操作还可以防止跨它的其他获取和释放操作的重新排序。

标签: c++ atomic memory-model stdatomic


【解决方案1】:

是否有可能 2 个不同的线程接收到相同的 x_old 值 0?

这是不可能的,因为操作是原子的。它要么完全发生,要么根本不发生。

排序与之前/之后的加载/存储有关,由于您没有任何内容,因此排序在这里无关紧要。换句话说,x.fetch_add(1, std::memory_order_relaxed); 在这里也有同样的效果。

在当前 x86 上,无论 memory_order 是相同的 lock xadd 指令,lock 前缀提供原子性和排序。对于memory_order_relaxedlock 的排序部分是不必要的。

【讨论】:

  • 所以 x.exchange(1, std::memory_order_relaxed) 也是原子的,在这里排序无关紧要?
  • @DerekZhang 这就是答案所说的,你猜对了。
  • 谢谢!我有另一个问题。线程 A 先运行 x.store(1, std::memory_order_release) ,然后线程 B 运行 x.load(std::memory_order_acquire) 。线程 B 中的 x 不保证 A 读取 1 个存储。如果我使用 memory_order_seq_cst,它会保证读取 1。对吗?
  • @DerekZhang 最好将另一个问题作为问题发布,而不是评论。
【解决方案2】:

对内存执行的任何操作都是在处理器内部完成的。即使是原子操作,处理器也会读取、修改和写回新值。如果操作失败(取决于实现,它可能不会失败,而是阻塞),它会重复自己。如果成功,为了操作正确,新值必须是前一个值,按请求修改然后存储。修改后的值返回给用户。处理器没有理由再次从内存中读取并从随机时间返回值。如果返回的值不是前一个值,则结果操作将不正确。

您可以使用以下方法对其进行测试:

long repeats = 1000000000;
long x = 0;
long sum = 0;
void *test_func(void*arg){
    long local_sum = 0;
    for (int i = 0; i < repeats; ++i) {
        local_sum += atomic_fetch_add_explicit(&x, 1, memory_order_relaxed);
    }
    atomic_fetch_add(&sum, local_sum);
    return NULL;
}

如果结果与顺序执行相同,则一切正常。

    long correct_res = 0;
    for (int i = 0; i < repeats * no_threads; ++i) {
        correct_res = correct_res + i;
    }

完整代码:

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>

long repeats = 1000000000;
long x = 0;
long sum = 0;
void *test_func(void*arg){
    long local_sum = 0;
    for (int i = 0; i < repeats; ++i) {
        local_sum += atomic_fetch_add_explicit(&x, 1, memory_order_relaxed);
    }
    atomic_fetch_add(&sum, local_sum);
    return NULL;
}

int main() {
    long correct_res = 0;
    for (int i = 0; i < repeats * 2; ++i) {
        correct_res = correct_res + i;
    }
    pthread_t pthread[2];
    pthread_create(&pthread[0], NULL, test_func, NULL);
    pthread_create(&pthread[1], NULL, test_func, NULL);

    pthread_join(pthread[0], NULL);
    pthread_join(pthread[1], NULL);
    printf("correct res : %ld\n res : %ld\n", correct_res, sum);
    if(correct_res == sum)
        printf("Success.\n");
    else
        printf("Failure.\n");
    return 0;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-22
    • 2015-10-24
    • 2021-11-02
    • 1970-01-01
    相关资源
    最近更新 更多