通过 _Atomic 指针修改非原子结构是否会产生数据争用?

Does Non-atomic struct modification via _Atomic pointer produce data-race?

我试图了解 C11 内存模型的工作原理并编写了两个函数,其中包含 conflict(在 5.1.2.4(p4) 的意义上)的表达式:

struct my_struct{
    uint64_t first;
    int64_t second;
} * _Atomic instance;

void* set_first(void *ignored){
    uint64_t i = 0;
    while(1){
        struct my_struct *ms = atomic_load_explicit(&instance, memory_order_acquire);
        ms -> first = i++;
        atomic_store_explicit(&instance, ms, memory_order_release);
        sleep(1);
    }
}

void* print_first(void *ignored){
    while(1){
        struct my_struct *ms = atomic_load_explicit(&instance, memory_order_acquire);
        uint64_t current = ms -> first;
        char buf[100];
        memset(buf, '[=10=]', sizeof(buf));
        sprintf(buf, "%" PRIu64 "\n", current);
        fputs_unlocked(buf, stdout);
        sleep(2);
    }
}

主要功能:

int main(void){
    struct my_struct tmp = {.first = 0, .second = 0};
    atomic_init(&instance, &tmp);
    printf("main\n");
    pthread_t set_thread;
    pthread_create(&set_thread, NULL, &set_first, NULL);

    pthread_t print_thread;
    pthread_create(&print_thread, NULL, &print_first, NULL);
    while(1){
        sleep(100);
    }
}

所以我试图证明程序是否包含没有数据竞争。以下是我的想法:

  1. 我们知道原子对象的释放操作与对象的获取操作是同步的。因此 set_first 中的 atomic_store_explicit(&instance, ms, memory_order_release); print_first 中的 atomic_load_explicit(&instance, memory_order_acquire) 同步。

  2. 由于set_first函数中的副作用ms -> first = i++出现在程序文本中的atomic_store_explicit(&instance, ms, memory_order_release);之前,我假设它是sequenced-before吧(这个我不太清楚,没找到规范的参考)。

  3. 组合项目符号 1.2. 意味着 ms -> first = i++ 线程间发生在 之前atomic_load_explicit(&instance, memory_order_acquire);所以他们在之前发生关系。

  4. 应用数据竞争定义,我们得出结论,print_first 函数中的冲突操作 uint64_t current = ms -> first;set_first 函数中的 ms -> first = i++; 不会产生数据竞争。

所以行为似乎是明确定义的。

可疑的事情假设ms -> first = i++;顺序在atomic_store_explicit(&instance, ms, memory_order_release);之前,因为它们一个接一个地发生在程序文本。

是否正确或程序包含数据竞争?

通过非_Atomic 指针修改非原子对象不是固有的 数据争用 UB。 (例如,您可以使用一种算法 int *p = shared_ptr++; 让每个线程在非原子数组中获取自己的插槽。)

但是在这个案例中,你有一个明显的UB案例,因为你有2个线程访问main的tmp.first,而且他们不是都读。


具有 mo_release 的存储在任何先前的存储(和加载)之后排序,包括像 ms->first = ... 这样的非原子存储。这就是 release-stores 与 relaxed 的区别。

但是您推理中的缺陷在步骤 1 中:atomic_store_explicit(&instance, ms, memory_order_release)set_first 中仅 同步获取负载存储的值! 在其他线程中获取负载 不要神奇地等待尚未发生的释放存储。保证是 if/when 你 加载发布存储 1 存储的值,你还可以从该线程看到所有早期的东西.

如果获取加载发生在发布存储之前(在全局顺序中,如果有的话),则不会与之同步。

两个线程中的acquire-load可以同时发生,然后狐狸在鸡舍:ms -> first = i++;uint64_t current = ms -> first;是运行 没有同步。

写入线程稍后将执行释放存储以将相同的值存储回 instance

完全无关紧要

脚注 1: (标准中的 "release-sequence" 语言将此扩展为查看修改初始发布存储结果的 RMW 操作的结果,等等。)


对于其他线程而言,set_first中的atomic_load_explicit基本无关紧要。您不妨将其吊出循环。